data-ui

Data-UI

Describe your data model once. Get a complete RBAC-backed application.

Repo at github.com/macnod/data-ui.

Table of Contents

Overview

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.

Core Philosophy

You describe your entities, relations, and UI behavior in one place. Then, Data-UI:

  1. Merges your model with a complete RBAC base model (*base-model*)
  2. Enriches types with default fields (:id, :created-at, :updated-at)
  3. Resolves references and generates join tables/views
  4. Produces ready-to-run SQL and pre-compiled validation logic
  5. Stores everything in *compiled-model* for fast runtime use

Generic endpoints like /api/list?type=todos and /api/schema/todos/add-form work for any type — including the built-in RBAC tables themselves.

Example Model

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:

The full RBAC system (:users, :roles, :permissions, :resources, and all join tables) is automatically included from *base-model*.

Example Compilation Results

This section presents some tiny pieces of the resulting enriched model, after compilation with (set-model *model*.

Create Table SQL for :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 $$;
     "
  )
⋮

View SQL for :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")))))

Fields Enrichment for :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))))

How It Works

Key Model Features

API Approach

All endpoints stay generic — no per-type handler generation needed:

React (or any frontend) can fetch the schema once and render forms/lists automatically.

Current Status (March 2026)

The active implementation lives in lisp/workbench.lisp. Ignore all other files for now.

Goals & Vision

Business & Monetization

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!

License

MIT