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.

Target MVP date: December 2026.

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.

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