Your whole app, in an email.
Describe your data once. Get a deployed, RBAC-backed application — deterministically. Manage users, roles, and permissions live, in the running app.
This is no longer just a thesis. As of July 2026, the full pipeline works end to end: a one-page model compiles into a complete application — PostgreSQL, REST API, RBAC, React frontend — and one command deploys it to Kubernetes behind TLS at its own domain. The first deployed instance is a to-do app whose entire description fits on a napkin.
Repo at github.com/macnod/data-ui.
Building solid, evolving, RBAC-heavy collaborative applications requires holding a web of invariants — every role against every resource against every operation, changing over time — consistent across thousands of lines of code. This is the part that is genuinely hard, and it is the part that breaks under iteration, whether the iteration is done by a human or by an AI.
Data UI lets you express the entire application as a small, reviewable artifact that fits comfortably in the body of an email, and guarantees that the expansion of that artifact into a running system is correct. You describe your application (entities, relationships, UI hints, etc.) once. The compiler produces the database, the API, the RBAC enforcement, the frontend, and the deployment — deterministically, with no per-type boilerplate and no hidden permission bugs.
Change the model, recompile, and everything updates consistently. The model is the DNA of the application. At less than a page of code for many applications, that DNA is tiny compared to the many thousands of lines that would otherwise be needed to describe such an application.
The 40,000-line application is dead. The 40-line model that compiles into one just won.
Change one file. Recompile. Every schema, every endpoint, every permission check, every UI form updates together — because they were never separate things.
An application that once required a team, a quarter, and a budget now fits in the body of an email and deploys in one command.
Before Data UI: “We’ll need a backend engineer, a frontend engineer, a DevOps person, and six weeks.” After Data UI: “I just sent you the model.”
You don’t maintain a Data UI application. You maintain a 40-line description, and the running system is a pure function of that description. The concept of “maintenance” as we know it just disappeared.
AI can now ship production applications. Not suggest code. Not draft migrations. Ship. The missing piece was never the AI — it was the invariant engine that lets the AI’s output be trusted.
The bottleneck in building this class of application is not code generation. A modern AI can emit plausible code all day. The bottleneck is specification compression and invariant enforcement.
An AI is good at producing a 40-line model. It is bad at producing a 40,000-line application whose permissions remain globally consistent as the application evolves, because it pattern-matches locally and drifts globally — it has no enforcement mechanism. A smarter model does not close this gap; it just drifts more eloquently.
Data UI closes the gap by reducing the dimensionality of the thing that has to be gotten right. With Data UI, the AI operates in the regime where it is strong — producing a small, structured model — and the compiler handles the regime where the AI is weak — expanding that model into a system with globally consistent RBAC and relational integrity.
This is the same relationship a programmer has with a type checker: even a superhuman programmer benefits from offloading invariant-enforcement to a deterministic tool. AI needs a substrate like this — and Data UI is it.
In practice this means the model format is an API for a non-human consumer. An AI does not write arbitrary code into a Data UI application; it selects from a defined vocabulary and fills in parameters, exactly as it fills a tool call. See Hooks and the Registry.
If you aim to develop solid, dependable, performant, maintainable, database-backed, ready-to-deploy applications that include full support for Role-Based Access Control (RBAC), and you want a deterministic development process (no countless iterations with an AI only to have to fix the difficult problems yourself in the end), then Data UI is your friend.
Data UI is a Common Lisp system that takes a simple nested plist model and compiles it into a full, production-ready data application:
:scope :user) for per-user data
filteringNo manual migrations. No per-type boilerplate. Change the model, call
(set-model "todos"), and everything updates deterministically. And this
is not a half-built promise: write the model, compile it, run
scripts/data-ui deploy, and minutes later your application is serving
real users over TLS at its own domain. We know because that is exactly
how the live demo got there.
You describe your entities, relations, and UI behavior in one place. Then, Data UI:
*base-model*):id, :created-at, :updated-at)*compiled-model* for fast runtime useGeneric endpoints like /api/list?type=todos work for any type — including
the built-in RBAC tables themselves.
Data UI deliberately supports two audiences through a single compiler:
The expert, self-hosting tier. Written in Common Lisp, the open-source engine gives you full power. You can attach raw Lisp lambdas as hooks and validations, override any lifecycle operation with your own function, and do anything the language allows. The guardrail here is your own experience and judgment. This tier is a shotgun: it does not stop you from doing whatever you want.
The AI / no-code / hosted tier. Here the model is pure data (YAML or JSON), hooks are chosen from a curated, parameterized registry, and there is no raw-code escape hatch. This constraint is not a limitation — it is what makes the tier safe to operate at scale and consumable by an AI. When a hosted user needs power beyond the data vocabulary, the escape valve is to self-host the open engine.
Both tiers reduce to the same contract before anything runs, so the compiler never special-cases one against the other.
Why Common Lisp stays. Despite its age, Common Lisp remains the most powerful programming language in a purely technical sense. Its object system, condition system, live image, incremental compilation of running code to efficient machine code, and full programmability have no real peers. Without that power gap, Data UI would not exist.
That the power of Common Lisp is invisible to most working programmers is itself a moat: it keeps casual competitors out while steering sophisticated users who want the capability without becoming a Lisp shop straight to the hosted product. The open engine remains fully available for those who want that power unmediated.
This example matches models/todos.lisp (and models/default-model.lisp,
which is an exact copy used by the deploy pipeline). Each file in the
models/ directory holds a bare model plist (no defparameter and no
wrapping variable). The top-level keys (:title, :name, :version,
:domain, :repl, :landing-page) carry the model’s identity, and :types holds the type
definitions. Load the model with (set-model "todos") — pass just the file
name, with no path and no .lisp extension. Prefer :repl nil in production
(see Deployment).
(:title "To Do List"
:name "todos"
:version "0.1"
:domain "todo.demo.data-ui.com"
:repl t
:landing-page :todos
:types
(:todos
(:table t
:create :auto :update :auto :delete :auto :display t
:type-roles ("todo-users")
:views (:main (:tables (:todos :todo-tags :tags))
:tags (:tables (:tags)))
:fields
(:name
(:type :text :identity t
:ui (:label "To Do" :input-type :line)
:validations (:required
(lambda (type-key field-key value user)
(declare (ignore user))
(unless (< (length value) 20)
(validation-error-string
type-key field-key value
"must be less than 20 characters."))))
:source (:view :main :column :name :agg :first)
:column t :not-null t :unique t)
:points
(:type :integer :default 0
:ui (:label "Points" :input-type :line)
:validations (:required)
:source (:view :main :column :points :agg :first)
:column t :not-null t)
:done
(:type :boolean :default :false
:ui (:label "Done" :input-type :check-box)
:source (:view :main :column :done :agg :first)
:column t :not-null t)
:tags
(:type :list
:ui (:label "Tags" :input-type :checkbox-list)
:validations (:join-items-exist)
:source (:view :main :table :tags :column :name :agg :list)
:source-all (:view :tags :table :tags :column :name :agg :list)
:join-table :todo-tags))
:list-form (:fields t)
:update-form (:fields t)
:add-form (:fields t))
:tags
(:table t
:create :auto :update :auto :delete :auto :display t
:type-roles ("todo-users")
:fields
(:name
(:type :text :identity t
:ui (:label "Tag" :input-type :line)
:validations (:required)
:source (:view :main :table :tags :column :name :agg :first)
:column t :not-null t :unique t))
:list-form (:fields t)
:update-form (:fields t)
:add-form (:fields t))
:todo-tags
(:table t :is-joiner t :internal t
:fields
(:reference (:target :todos)
:reference (:target :tags)))))
This single definition aims to give you:
updated_at triggers:main) that pull related data like tags without extra queriesmacnod/rbac):label, :input-type, form layouts) that a React frontend can read directly to generate dynamic forms and listsThe full RBAC system (:users, :roles, :permissions, :resources, and associated
join tables) is automatically included from *base-model*. A user settings table is also
included.
This section presents some tiny pieces of the resulting enriched model, after
compilation with (set-model "todos").
:todos(:TODOS
(:CREATE-TABLE-SQL
(:TABLE "
create table if not exists rt_todos (
id uuid primary key not null references resources(id) on delete cascade,
created_at timestamp not null default now(),
updated_at timestamp not null default now(),
todo_name text not null unique,
todo_points integer not null default 0,
todo_done boolean not null default 'false'
)
"
:TRIGGER "
do $$
begin
if not exists (
select 1 from pg_trigger
where tgname = 'set_rt_todos_updated_at'
and tgrelid = 'rt_todos'::regclass::oid
) then
create trigger set_rt_todos_updated_at
before update on rt_todos
for each row
execute function set_updated_at_column();
end if;
end $$;
")
:todos(:VIEWS
(:MAIN
(:TABLES (:TODOS :TODO-TAGS :TAGS) :SQL "
select
rt_todos.id rt_todos_id,
rt_todos.created_at rt_todos_created_at,
rt_todos.updated_at rt_todos_updated_at,
rt_todos.todo_name rt_todos_todo_name,
rt_todos.todo_points rt_todos_todo_points,
rt_todos.todo_done rt_todos_todo_done,
rt_todo_tags.id rt_todo_tags_id,
rt_todo_tags.created_at rt_todo_tags_created_at,
rt_todo_tags.updated_at rt_todo_tags_updated_at,
rt_todo_tags.todo_id rt_todo_tags_todo_id,
rt_todo_tags.tag_id rt_todo_tags_tag_id,
rt_tags.id rt_tags_id,
rt_tags.created_at rt_tags_created_at,
rt_tags.updated_at rt_tags_updated_at,
rt_tags.tag_name rt_tags_tag_name
from rt_todos
left join rt_todo_tags on rt_todos.id = rt_todo_tags.todo_id
left join rt_tags on rt_tags.id = rt_todo_tags.tag_id"
:ALIASES
(:TODOS
(:ID :RT-TODOS-ID :CREATED-AT :RT-TODOS-CREATED-AT :UPDATED-AT
:RT-TODOS-UPDATED-AT :NAME :RT-TODOS-TODO-NAME :POINTS
:RT-TODOS-TODO-POINTS :DONE :RT-TODOS-TODO-DONE)
:TAGS
(:ID :RT-TAGS-ID :CREATED-AT :RT-TAGS-CREATED-AT :UPDATED-AT
:RT-TAGS-UPDATED-AT :NAME :RT-TAGS-TAG-NAME))
:COLUMNS
(:TODOS
(:ID "rt_todos.id" :CREATED-AT "rt_todos.created_at" :UPDATED-AT
"rt_todos.updated_at" :NAME "rt_todos.todo_name" :POINTS
"rt_todos.todo_points" :DONE "rt_todos.todo_done")
:TAGS
(:ID "rt_tags.id" :CREATED-AT "rt_tags.created_at" :UPDATED-AT
"rt_tags.updated_at" :NAME "rt_tags.tag_name")))
:TAGS
(:TABLES (:TAGS) :SQL "
select
rt_tags.id rt_tags_id,
rt_tags.created_at rt_tags_created_at,
rt_tags.updated_at rt_tags_updated_at,
rt_tags.tag_name rt_tags_tag_name
from rt_tags"
:ALIASES
(:TAGS
(:ID :RT-TAGS-ID :CREATED-AT :RT-TAGS-CREATED-AT :UPDATED-AT
:RT-TAGS-UPDATED-AT :NAME :RT-TAGS-TAG-NAME))
:COLUMNS
(:TAGS
(:ID "rt_tags.id" :CREATED-AT "rt_tags.created_at" :UPDATED-AT
"rt_tags.updated_at" :NAME "rt_tags.tag_name")))))
:todos :fields :name(:TODOS
(:FIELDS
(:NAME
(:BASE-FIELD NIL :UI (:LABEL "To Do" :INPUT-TYPE :LINE) :UNIQUE T
:PRIMARY-KEY NIL :TARGET NIL :JOIN-TABLE NIL :VALIDATIONS
(#<FUNCTION V-TYPE> #<FUNCTION V-REQUIRED>
#<FUNCTION (LAMBDA (TYPE-KEY FIELD-KEY VALUE USER)) {B80133ADAB}>)
:FORCE-SQL-NAME NIL :NAME-SQL "todo_name" :TYPE-SQL "text" :CREATE-SQL
"todo_name text not null unique" :SOURCE
(:VIEW :MAIN :COLUMN :NAME :AGG :FIRST :ALIAS-KEY :RT-TODOS-TODO-NAME
:COLUMN-NAME "rt_todos.todo_name")
:SOURCE-ALL NIL :TYPE :TEXT :COLUMN T :NOT-NULL T :REFERENCE NIL :DEFAULT
:NULL))))
set-model (in lisp/model.lisp) — Compiles the model, enriches it, generates all SQL/views, and stores the result in *compiled-model*.:reference into proper foreign keys, builds joined view SQL, prepares parameterized CRUD statements.be-list, be-insert, be-update, be-delete, be-item, etc. in lisp/backend.lisp) pull pre-generated SQL from the compiled model.user-allowed from the rbac library. RBAC tables are treated exactly like your own types, so you can manage users, roles, permissions, and resource access through the same UI/API.set-model. Separate validation functions are available.*compiled-model* actually isThe compiler stores its output in *compiled-model* — a single structure
that is simultaneously the application specification (data), the
deployment configuration (data), and the executable application logic
(native machine code). SBCL compiles every backend function, every RBAC
check, and every hook lambda — including validation and lifecycle hooks
authored directly in the model — to native x86-64 or ARM instructions.
No interpreter. No VM. No JIT warmup. When a validation hook runs, it
calls a function pointer to compiled code that was placed in the model
at compile time. The model is not just a description of the application;
it is the application, in executable form.
For a detailed comparison of Data UI’s approach against existing tools, see Competitive Landscape.
:reference instead of manual IDs for clean relations:target as a shorthand for :reference on non-joiner fields (sets up FK + UUID column):views to explicitly control joins (e.g., :main (:tables (:todos :todo-tags :tags))):scope :user on a view to filter be-list results to records owned by the current user:scope :user on a field’s :source to filter aggregated field values to the current user (e.g. “my rating”):identity t marks a field as the natural key used for write-through lookups and unique indexes:write-to declares related-table upserts from a field write (e.g. rating → ratings row); non-transactional in MVP:ui hints (:label, :input-type, :render-as) for frontend rendering:render-as values: :code, :image, :image-list — trigger specialized frontend rendering (code blocks, thumbnail grids, lightbox preview):input-type values: :line, :textbox, :select, :check-box, :checkbox-list, :read-only, :file, :hidden:validations common validation names, parameterized registry entries, or lambdas that validate form/field data:join-table for many-to-many relationships:is-joiner t for explicit join tables:tree t / :is-leaf / :parent-type / :fs-backed t for tree-structured types with filesystem backing (directories, file storage):path t to mark the path field on fs-backed types:autofill :user to auto-populate a field with the current username:per-user t (type-level) to suppress the roles field (used by settings):type-roles to declare which roles can access a type:landing-page (top-level) to declare which type the frontend shows on load (resolved per-user via be-landing-page):force-sql-name to override the generated SQL column name:auto for create/update/delete → generated SQL (or override with your own function):create, :update, :delete, :post-create, :pre-delete, etc.) that accept registry entries or raw functions (shell hooks planned)rt_ prefix to avoid name collisions with RBAC tablesCustom logic — validation and lifecycle behavior — attaches through hooks. Every hook, whatever its surface form, reduces to a single calling contract before it runs, so the compiler treats them uniformly.
There are three ways to express a hook, spanning the two tiers:
| Form in the model | Who writes the Lisp | Tier | Status |
|---|---|---|---|
(:keyword args...) |
the registry author (you/community) | AI / no-code / hosted | Supported |
(lambda ...) |
the model author, raw | expert / self-host only | Supported |
(:shell "script" args...) |
nobody (compiler generates adapter) | AI / no-code / hosted | Planned |
A validation hook conforms to:
(lambda (type-key field-key value user) -> nil | error-string)
A lifecycle hook conforms to (for example):
(lambda (type-key data user &key roles) -> effect)
Returning nil (or no error) means success; returning an error string fails the
operation. Hooks are lists, so multiple hooks can be attached and each reduces to
this contract.
MVP caveat — transactions deferred: lifecycle hooks are not transaction-wrapped. If one hook in a list fails, the operation fails without rollback of the primary write or earlier hooks. The same rule applies to write-through (
:write-to): the primary row commits first; related-table upserts run after and are best-effort. Transactions and rollback (including idempotent database initialization) are deliberately deferred to post-MVP. The eventual transaction boundary is intended to wrap primary write + hook list + write-through as a unit; design hooks with that future in mind, and never assume atomicity in MVP code or docs.
The registry generalizes the existing keyword-to-lambda pattern used for validations. A registry entry is a named factory that closes over parameters supplied as data and returns a contract-conforming closure.
For example, a maximum-length validation written as pure data:
:validations (:required (:max-length 20))
is backed by a registry entry whose Lisp lives in the engine, written once:
(register-hook :max-length
(lambda (max) ; parameter from the model
(lambda (type-key field-key value user) ; conforms to the contract
(unless (< (length value) max)
(validation-error-string type-key field-key value
(format nil "must be less than ~d characters." max))))))
The model author wrote only data — (:max-length 20) — which serializes cleanly
to YAML or JSON. The same pattern applies to lifecycle hooks:
:post-create (:add-user-settings) ; zero-arg entry
:post-create ((:send-webhook :url "https://...") ) ; parameterized entry
Each registry entry carries three things:
The parameter schema does triple duty:
This is the mechanism that makes the model AI-consumable: an AI does not write hooks, it picks registry entries and fills parameters. The Lisp lives in the registry; the model author — human or AI — writes only data.
Shell hooks are planned but not yet supported. The intended form is a tagged
expression such as (:shell "thumbnail.sh" ...). When implemented, the compiler
will generate an adapter that wraps the subprocess to satisfy the same hook
contract as any other hook: input as JSON on stdin; exit status and output
determine success or failure. Until then, (:shell ...) forms are rejected at
compile time.
All endpoints stay generic — no per-type handler generation needed:
GET /api/list?type=todos → RBAC-gated results from the compiled view, including schema (list-form, add-form, update-form, allowed-values) and permission flags (create, delete, update)GET /api/item, /api/id, /api/value, /api/value-id, /api/column → targeted data retrievalPOST /api/insert, /api/update, /api/delete → CRUD mutations (validation runs first)POST /api/upload → file upload (multipart, returns file-token)POST /api/validate-field, /api/validate-form → per-field and per-form validationGET /api/types, /api/info → schema and metadataPOST /api/login, /api/refresh → JWT auth (access + refresh tokens)GET /api/file → file serving (with token auth)GET /health → health checkReact (or any frontend) fetches items with their schema and renders
forms/lists automatically. The :ui plist on each field is the extension
point — :render-as, :input-type, and :table are consumed directly by
the frontend components.
Start a repl-environment terminal
cd data-ui scripts/data-ui repl
M-x slime-connect RET localhost RET 4010
(set-model "todos")(run-tests)Build the frontend (one-time, or after frontend changes)
cd data-ui/web
npm install
npm run build
(start-web-server)Deployment is part of the compiler’s promise, not an afterthought. The model itself declares the application’s identity:
(:title "To Do List"
:name "todos"
:version "0.1"
:domain "todo.demo.data-ui.com"
;; Prefer :repl nil in production (extra attack surface; SSH tunnel still required)
:repl t
:landing-page :todos
:types ...)
and one command turns that into a running, public application:
scripts/data-ui deploy
Behind that command: the model is compile-checked against a throwaway
database, the release is tagged from the model’s version plus the git
hash, a Docker image is built (React frontend compiled in one stage,
precompiled SBCL runtime in another), Kubernetes manifests are rendered
from templates and applied to a k3d cluster (each instance in its own
namespace, with its own PostgreSQL and persistent volumes), and HAProxy
routing is updated so the model’s :domain serves the app over TLS — a
wildcard Let’s Encrypt certificate that renews itself.
The deploy is deterministic and repeatable: every fact is derived from the model and the git commit. Secrets and port assignments are generated once and thereafter recovered from the live cluster, so a deploy can be re-run from a fresh machine without breaking a running instance.
:repl t works and exposes Swank for the instance (reachable over an SSH
tunnel). Prefer :repl nil in production — it is an extra attack surface
even behind a tunnel.
The full story — every step, every file, where the admin password lives, how cert renewal works, troubleshooting — is in docs/deployment.md.
The project is in active development, and the core claim is now demonstrated end to end:
:scope :user filters be-list results to records owned by
the current user. Field-level scoping (:scope :user on a field’s
:source) filters aggregated field values to the current user (e.g.
“my rating” on Model Bank). It does not control field visibility or
editability in the UI.:write-to + :identity t) is implemented: related-
table upserts run from be-insert / be-update (best-effort, non-
transactional). Used by Model Bank ratings. Some edge cases (e.g.
clear-to-NULL) remain open.models/modelbank.lisp):
tree-structured types with filesystem backing (:tree, :is-leaf,
:parent-type, :fs-backed), path fields (:path), auto-populated
fields (:autofill :user), per-user types (:per-user), write-through
ratings (:write-to, :identity), and UI hints for code blocks, images,
and image lists (:render-as).multipart/form-data
POST to /api/upload, then a JSON /api/insert carrying the returned
file-token). File update is not yet implemented and may be
deferred past the MVP.tests/ (FiveAM): predicate-tests.lisp, backend-tests.lisp,
rest-tests.lisp, scoping-tests.lisp, plus helpers.lisp and
model-template.lisp. One view-level scoping behavioral test remains
flaky / TODO.Model compilation, SQL generation for tables/views/triggers, RBAC integration, validation, CRUD, write-through, and Kubernetes deployment are implemented and exercised. Work continues on Model Bank completion, write-through edge cases, UI refinement, and additional example models.
Deliberately deferred to post-MVP (do not assume these exist today):
:write-to) follows the same rule: primary write commits first;
related-table upserts are best-effort. Idempotent database
initialization is also deferred (see deployment Trap 1).ON CONFLICT upserts (blocked on the two-phase
resource insert).:shell ...) — planned; rejected at compile time today.See Hooks and the Registry for the hook contract and the MVP atomicity caveat.
See lisp/model.lisp for the current *base-model* and the models/
directory for example models (one per file, e.g. todos.lisp,
modelbank.lisp, widgets.lisp), each loadable with
(set-model "todos"), lisp/backend.lisp for the be-* API,
lisp/rest.lisp for HTTP endpoints, and the tests/ directory for
usage examples. Contributions welcome — this is early stage!
Target: a complete MVP by the end of December 2026, including a 30-second video that goes from nothing — no database, no code — to a deployed, working application.
Odds of hitting the date: strong. The reasoning, plainly:
Model Bank is the priority function. The MVP must prove that real, non-trivial applications can be built on Data UI significantly faster than any alternative — and the way to prove that is to build one. Model Bank (a model-sharing application with relationships, ownership, image association, and ratings) is that application. Gaps surfaced by building Model Bank are, by definition, the highest-priority work.
Data UI exists to solve a problem that existing low-code and backend tools handle poorly: building production-grade, multi-user applications that allow users to interact with each other, share resources, and that require robust, evolving role-based access control.
Most collaborative applications — internal tools, client portals, team workspaces, resource-sharing systems — need fine-grained permissions that change over time. Current low-code platforms either offer weak or bolted-on RBAC, or they generate large amounts of opaque code that must be manually finished and maintained. The result is slow iteration, hidden permission bugs, and painful refactoring when requirements change.
Data UI takes a different approach. You describe your data model, relationships, and UI hints in one small, reviewable plist. The system compiles this into:
Because RBAC entities (users, roles, permissions) are treated as first-class types, permission changes are made through the same interface as any other data — no separate admin layer or model edits required.
The model acts as the DNA of the application. Small, auditable changes produce deterministic, system-wide updates. This makes iteration fast and safe: refine your vision by editing the model rather than rewriting code.
For custom logic and external integrations, Data UI provides typed hooks that receive pre-evaluated authorization context and a well-defined payload. Developers attach behavior without rebuilding the core application architecture.
The result is a tool that lets technical users, small teams, and AI agents build reliable, RBAC-protected collaborative applications much faster and with greater long-term maintainability than traditional development or existing low-code platforms.
MVP target: December 2026. A minimal but production-capable system that delivers a complete RBAC-protected application (database, React frontend, and Kubernetes deployment) from a small model in under 30 minutes. The MVP ships with a 30-second video that goes from nothing to a deployed app. The deployment pipeline — historically the riskiest part of such a promise — is already working in production; see Road to MVP.
After the MVP, planned work includes a hosted service with JSON/YAML model input and AI prompts, a curated hook registry as the AI-and-no-code escape hatch, the marketplace described below, and professional support services.
The marketplace is the growth engine. It does three things at once: it solves onboarding by making the first experience copy a working thing rather than author from a blank page; it creates network effects; and it becomes a retrieval corpus that both humans and AI draw from — find a near-fit model, adapt it, change the appearance, deploy.
Creating a new application becomes: find a model in the marketplace, copy it, modify it slightly, optionally restyle it, and deploy.
We pursue the marketplace in two forms:
(a) An open-source reference Marketplace. Its job is not to be the product — it is to be the proof. You can stare at a fraction of a page of model and realize it represents the entire Marketplace application. Its smallness is the point and is defended as a feature. Being open and copyable, it is also the canonical first entry in the corpus — the template everyone forks.
(b) A closed-source, production-grade Marketplace. This is where iteration and revenue live: YAML/JSON model input, AI prompts, the corpus, search, hosting, and one-click deploy.
The line between them is crisp: the open reference app is the application logic; the closed product adds operational concerns (hosting, AI front door, billing, scaling, moderation) that are infrastructure, not application. Keeping that line clear is what lets the proof and the product reinforce each other rather than undercut the central claim.
The Data UI engine is and will remain fully open source under the MIT license. The core (model compiler, SQL generation, RBAC integration, CRUD layer, the reference Marketplace) is free for anyone to use, self-host, or modify.
The hosting is a separate, closed-source product. Initially it has no raw-code escape hatch: models are pure data, hooks come from the curated registry. When a hosted user needs power beyond the data vocabulary, the escape valve is to self-host the open engine.
Initially we will focus on building custom applications for clients while dogfooding the tool on our own projects.
After the MVP, to fund continued development and provide additional value to users, we plan to provide:
If you’re building internal tools or client apps and want help, feel free to reach out. Contributions and feedback are very welcome — this is still early stage!
MIT