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 compiles 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 what’s assigned to the *model* variable in lisp/workbench.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 :todo-name :agg :first)
:column t :not-null t :unique t)
:tags (:type :list
:ui (:label "Tags" :input-type :checkbox-list)
:source-sel (:list (:view :main :column :tag-name))
:source-all (:list (:table :tags :column :tag-name))
:ids-table :tags
:join-table :todo-tags))
: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)))
:tags
(:table t
:create :auto :update :auto :delete :auto
:fields (:name (:type :text
:ui (:label "Tag" :input-type :line)
:column t :not-null t :unique t))
:list-form (:fields t)
:update-form (:fields t)
:add-form (:fields t))))
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*.
: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_todo_id,
rt_todos.created_at rt_todo_created_at,
rt_todos.updated_at rt_todo_updated_at,
rt_todo_name,
rt_tags.id rt_tag_id,
rt_tags.created_at rt_tag_created_at,
rt_tags.updated_at rt_tag_updated_at,
rt_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
"
:COLUMNS
(:TODOS (:RT-TODO-ID "rt_todo_id"
:RT-TODO-CREATED-AT "rt_todo_created_at"
:RT-TODO-UPDATED-AT "rt_todo_updated_at"
:RT-TODO-NAME "rt_todo_name")
:TAGS (:RT-TAG-ID "rt_tag_id"
:RT-TAG-CREATED-AT "rt_tag_created_at"
:RT-TAG-UPDATED-AT "rt_tag_updated_at"
:RT-TAG-NAME "rt_tag_name")))
:TAGS
(:TABLES (:TAGS)
:SQL
"
select
rt_tags.id rt_tag_id,
rt_tags.created_at rt_tag_created_at,
rt_tags.updated_at rt_tag_updated_at,
rt_tag_name
from rt_tags
"
:COLUMNS
(:TAGS (:RT-TAG-ID "rt_tag_id"
:RT-TAG-CREATED-AT "rt_tag_created_at"
:RT-TAG-UPDATED-AT "rt_tag_updated_at"
:RT-TAG-NAME "rt_tag_name")))))
:todos :fields :name(:TODOS
(:FIELDS
(:NAME
(: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
:SQL-NAME "rt_todo_name"
:SQL-ALIAS "rt_todo_name"
:TYPE-SQL "text"
:CREATE-SQL "rt_todo_name text not null unique"
:ALIAS-KEY :RT-TODO-NAME
:SOURCE (:VIEW :MAIN :COLUMN :TODO-NAME :AGG :FIRST)
:TYPE :TEXT
:COLUMN T
:NOT-NULL T
:REFERENCE NIL))))
set-model — 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) 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 be-validate (full form) and be-validate-field (real-time single field) functions.: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 be-validate first, then use compiled SQLReact (or any frontend) can fetch the schema once and render forms/lists automatically.
be-list-internal and supporting helpers are in placemacnod/rbac (including user-allowed, list functions, and with-rbac)be-* CRUD functionsThe active implementation lives in lisp/workbench.lisp. Ignore all other files for now.
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