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 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.

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 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:

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*).

(The example output remains accurate to the current implementation in lisp/model.lisp.)

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_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")))))

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

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 (May 2026)

The project is still in active development. Recent progress includes:

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!

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