Describe your data model once. Get a complete RBAC-backed application.
Repo at github.com/macnod/data-ui.
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 will compile it into a full production-ready data application:
No manual migrations. No per-type boilerplate. Change the model, call (set-model *model*), and everything updates deterministically. Data UI aims to let you write the model, compile it into an application, and deploy the application in a half hour.
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 and /api/schema/todos/add-form work for any type — including the built-in RBAC tables themselves.
This example is from the *model* definition in lisp/model.lisp.
(defparameter *model*
`(:todos
(:table t
:create :auto :update :auto :delete :auto
:views (:main (:tables (:todos :todo-tags :tags))
:tags (:tables (:tags)))
:fields (:name (:type :text
:ui (:label "To Do" :input-type :line)
:source (:view :main :column :name :agg :first)
:column t :not-null t :unique t)
:tags (:type :list
:ui (:label "Tags" :input-type :checkbox-list)
:source (:view :main :table :tags :column :name :agg :list)
:source-all (:view :tags :table :tags :column :name :agg :list)
:ids-table :tags
: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
:fields (:name (:type :text
:ui (:label "Tag" :input-type :line)
: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
: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 all join tables) is automatically included from *base-model*.
This section presents some tiny pieces of the resulting enriched model, after compilation with (set-model *model*).
(The example output remains accurate to the current implementation in lisp/model.lisp.)
:todos(:TODOS
(:CREATE-TABLE-SQL
(:TABLE
"
create table if not exists rt_todos (
id uuid primary key default uuid_generate_v4(),
created_at timestamp not null default now(),
updated_at timestamp not null default now(),
rt_todo_name text not null unique
)
"
: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_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
join rt_todo_tags on rt_todos.id = rt_todo_tags.todo_id
join rt_tags on rt_todo_tags.tag_id = rt_tags.id
"
:ALIASES
(:TODOS
(:ID :RT-TODOS-ID :CREATED-AT :RT-TODOS-CREATED-AT :UPDATED-AT
:RT-TODOS-UPDATED-AT :NAME :RT-TODOS-TODO-NAME)
: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")
: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 :CHECKED NIL :UNCHECKED NIL :UI
(:LABEL "To Do" :INPUT-TYPE :LINE) :UNIQUE T :PRIMARY-KEY NIL :FS-BACKED
NIL :TARGET NIL :IDS-TABLE NIL :JOIN-TABLE NIL :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-SEL NIL :SOURCE-ALL NIL :TYPE :TEXT :COLUMN T :NOT-NULL T
:REFERENCE NIL))))
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.:reference instead of manual IDs for clean relations:views to explicitly control joins (e.g., :main (:tables (:todos :todo-tags :tags))):ui hints (:label, :input-type :checkbox-list, etc.) for frontend rendering:validation common validation names or lambdas that validate form/field data:join-table / :ids-table for many-to-many relationships:is-joiner t for explicit join tables:auto for create/update/delete → generated SQL (or override with your own function)rt_ prefix to avoid name collisions with RBAC tablesAll endpoints stay generic — no per-type handler generation needed:
GET /api/schema/:type/:form → JSON with fields, UI hints, and validation rules (for dynamic React forms)GET /api/list?type=todos → RBAC-gated results from the compiled viewPOST /api/validate, /api/insert, /api/update → call validation first, then use compiled SQLReact (or any frontend) can fetch the schema once and render forms/lists automatically.
The project is still in active development. Recent progress includes:
tests/predicate-tests.lisp and tests/backend-tests.lisp (using FiveAM).web/ directory.Model compilation, SQL generation for tables/views/triggers, basic RBAC integration, validation, and basic CRUD are implemented and tested. Work continues on fully solidifying the REST API, completing the React frontend, producing Kubernetes manifests, and achieving the full end-to-end vision.
See lisp/model.lisp for the current *model* and *base-model*, lisp/backend.lisp for the be-* API, and the tests/ directory for usage examples. Ignore outdated references in older files. Contributions welcome — this is early stage!
Data UI is and will remain fully open source under the MIT license. The core (model compiler, SQL generation, RBAC integration, CRUD layer, etc.) is free for anyone to use, self-host, or modify.
Initially we will focus on building custom applications for clients while dogfooding the tool on our own projects.
MVP target: December 2026. Goal is a minimal but production-capable system that delivers a complete RBAC-protected application (with database, React frontend, and Kubernetes deployment) from a small model in under 30 minutes.
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