Hot Glue: A Scaffold Companion for Turbo-Rails and Hotwire

Instantly Write Turbo-Rails Scaffold for Admin Views + More



Join Newsletter

Turbo-Rails is a bold new vision for Ruby on Rails and both web and mobile development. It is based on an old concept from the early days of computing — dating back to the 1970s. The concept is called “dumb terminal” and it means that the computer screen you use is for display only. In the ‘dumb terminal’ world, the majority of the computing will happen in the cloud.

Alternatively, today’s modern JS-heavy environments (React, Vue, Ember, etc), megabytes and megabytes of Javascript code gets loaded into your browser. That code has to maintain all kinds of application logic and, most importantly, a context state. That means that no matter what you do, you have to worry a lot about writing your business logic in Javascript

Turbo-Rails is the opposite of that idea. In Turbo-Rails, you’ll keep your business and display logic on the server, and treat the browser as a terminal that is there to display your UI only— it is ‘dumb.’ Whether or not Turbo-Rails will rival the new kids on the block will remain to be seen, but it sure is fast. These days, I think most of the process of building a great app involves user feedback, product-market fit, and iterating to make sure your users actually want what you’re building. The power of Rails lies in ActiveRecord and migrations, and thus it is an ideal tool for putting together prototypes quickly. The problem is, Rails has its own idiosyncratic syntaxes that make getting started challenging for those curious about this old framework. Do I start with Turbolinks or Turbo Rails or just plain page reload interactions? How about I treat Rails as the API-only and build my own frontend? Should I use ActiveRecord’s relationships, even knowing that instantiating a lot of Rails objects can be slow?

The problem presented is that there are almost too many choices for people who just want to build their app. You have a vision for how to map your users’ mental models onto code, objects, or tuples (database records), and you have a vision for the interfaces, screens, and interactions. But you aren’t sure about all the Rails-syntax or when you get started in Rails you quickly get lost specific syntax, style. or architecture decisions to do basic things.

Meet Hot Glue

This gem, Hot Glue, is a rapid application prototype builder for Rails built using Turbo Rails, which will become the default Rails setup in the soon-to-be-released Rails 7. It assumes you know the basics of Rails — MVC, routes, etc — and want to build a Create-Read-Update-Delete (CRUD) set of operations on an object in your database. It handily comes with a LIST page with pagination, and does all of its operations as Edit-in-Place. This means the user never leaves the page. It gives you a simple, lego-like set of building blocks to formulate standard-looking dashboards. It stays within those bounds and tries to do what it does well and nothing else.

This blog post will introduce you to Hot Glue by walking you through six separate examples. Each of the examples demonstrates a different part of Hot Glue, and by the end you should have an understanding of how to use it.

If you are not yet familiar with Rails MVC or Rails routes, you should start with a basic Rails tutorial or the Rails guides.

Hot Glue has only 1 page of documentation — the README itself — which is the same as its Github home, here.

For this tutorial, you will need to scroll to the section marked “Getting Started” in the this README document, and follow allow there. I will reference those steps below briefly but not repeat all of the details.

I’m so pleased to see Hot Glue take off since I introduced it 2021:

This tool is quickly becoming the de-facto way to build admin interfaces in Ruby on Rails.

Getting Started

Remember, start with “Getting Started” in the README. As you go through each step, always remember to examine and check-in your code. That way you can easily see and understand what each step does.

First, if you’re on Rails 6, complete Step 1: Add Hotwire and Step 2: Switch from Turbolinks to Turbo-Rails

You’ll next want to add the hot glue gem (Step 3), and add the testing gems (Step 4). Next run the Hot Glue installer as described in Step 5(A).

Note that Steps 5(B), 5 (C), 5(D) have been done for you by the installer, but you should visually inspect the generated files to see what has happened and check them in group-by-group.

Step 6 is to decide on a theme layout. Hot Glue’s first implementation used Bootstrap, and you can still use Bootstrap 4 if you want use Hot Glue with the --layout=bootstrap flag during your installation. (This gets saved to config/hot_glue.yml, where you can easily change it.) When constructing scaffold with bootstrap layout (at this time Hot Glue peeks into config/hot_glue.yml to see what you’ve set there), your views come out with divs that have classes like .container-fluid, .row, and .col. You’ll need to install Bootstrap separately, any way you like, but jQuery is not required as Hot Glue does not rely on jQuery-dependant Bootstrap features.

If instead you install Hot Glue (or switch the setting) using the default layout mode (--layout=hotglue), your scaffolding will be built using no-Bootstrap syntax: It has its own syntax with classes like .scaffold-container, .scaffold-list, .scaffold-row, and .scaffold-cell

During the installation, if your --layout flag is left unspecified or set to hotglue (that means you didn’t set it to bootstrap), Hot Glue will also expect you to pass a --theme flag.

The themes are:

• like_bootstrap (inspired by Bootstrap 4)

• like_los_gatos (Netflix inspired)

• like_mountain_view (Gmail/Google Analytics inspired)

• dark_knight (inspired by 2008 film The Dark Night)

The install just copies the SCSS file from the gem into your app. You are free to modify it from there.

Step 7 is installing Font Awesome. (Hot Glue will work fine without the fonts because they are only decorative icons within some of the generated buttons.)

Finally, Step 8 is to add Enum support for Postgres. To do this, you’ll need to install another gem and know how to define your enums. This is covered in my blog post Enumerated Types in Ruby on Rails with Postgres. If you have no enum fields on your models then you can skip this step.

The very last thing to consider is Devise and authentication (Step 9), but that is not necessary for Example 1. Example 2 specifically covers how to install Devise and shows how access control works with your logged-in user.

Ok let’s dive in.

Example 1: Books & Authors

The Books & Authors example is the simplest of all of the examples: It contains only two tables (books & auth, and two scaffoldings. You will need to go through all of the setup steps in “Getting Started” above, except Step 9. For this example, we will use –god (or –gd) controllers, which means that they are like admin interfaces: By default, they will be able to create/read/update/delete all records on the tables we are constructing.

Remember, only use Gd controllers when you also have some kind of admin authentication. We will cover that example later; for Example 1, the Megatron is only a demo app so it will allow any user access to both tables (no authentication).

When you make your new Rails app, make it with

rails new BooksAndAuthors --database=postgresql

Then go through “Getting Started” above (except Step 9).

(Somewhere along the way during the setup make sure you rails db:create and rails db:migrate to get your schema started.)

Next, we’ll make two models & migrations using the Rails built-in model generators:

rails generate model Author name:string

Take a look at the 4 files that Rails has created or you:

This is a normal Rails migration that should look familiar. If it doesn’t, take a look at the Rails guide for migrations.

class CreateAuthors < ActiveRecord::Migration[6.1]
def change
create_table :authors do |t|
t.string :name


All we’re doing is creating a table of Authors who have a name. (The t.timestamps tells Rails to create both an updated_at and created_at timestamp for this table.)

Next run rails db:migrate to create the Authors table.

Before we get to building the scaffolding, let’s keep going and build out our Books table. Take a note that this Books table is just a small pretend part of a demo app. Its purpose is to demonstrate one of each type of field so you can see the all the field types in action.

rails generate model Book name:string author_id:integer blurb:string long_description:text cost:float how_many_printed:integer approved_at:datetime release_on:date time_of_day:time selected:boolean genre:enum

Don’t migrate the database again — YET!

We will need to fix the enum field before you can run the migration.

Now go ahead and find the migration file itself and edit it to add the enums.

Fix the Migration

In the Megatrons create migration, add code shown in orange below:

class CreateBooks < ActiveRecord::Migration[6.1]
  def change
    create_enum "genres", %w[Fiction Nonfiction Biography Science_Fiction Mystery]
    create_table :books do |t|
      t.string :name
      t.integer :author_id
      t.string :blurb
      t.text :long_description
      t.float :cost
      t.integer :how_many_printed
      t.datetime :approved_at :release_on
      t.time :time_of_day
      t.boolean :selected
      t.enum :genre, as: :genres


Now you can run rails db:migrate

Add the same Enum to the Model Books

Open app/models/book.rb and add this code:

class Book < ApplicationRecord
  include PGEnum(genres: %w[Fiction Nonfiction Mystery Romance Novel])

Now, while we’re in here, we’ll hook up our Books to Authors. As you might have guessed, Books belong_to an :author, and an Author has_many :books. If you aren’t familiar with Rails Active Record relationships, check out the Rails guide here.

class Book < ApplicationRecord
  include PGEmum(genres: %w[Fiction Nonfiction Mystery Romance Novel])
  belongs_to :author


class Author < ApplicationRecord
  has_many :books

Finally, let’s build the scaffolding:

Here are the two Hot Glue commands we’ll need:

rails generate hot_glue:scaffold Author --gd

rails generate hot_glue:scaffold Book --gd

Run them both and see what is output:

For every scaffolding you build, Hot Glue generates up to 16 files:

A controller
A spec file
The 16 files that Hot Glue may generate. *Top-level view

By design, the files that begin with underscore (_) are called partials, and they are used as sub-views of top-level views or other partials. Also by design, the top-level views (shown here with *) all expect to have instance variables made available to them (@) by the controller itself. Then, those top-level pages will in turn render lower level partials (which begin with underscores) and when they do, they pass the objects down the chain using local variables passed in-scope when the partial is rendered.

This is important because the lower-level partials (any views that are not top-level) should not reference instance variables. In most cases, the instance variables switch names from the instance variable name (@abc) to a local variable name (abc) when you move from the top-level views to the partials. This is by design and facilitates seamless re-use of the views using nesting.

The other thing to note is that the edit template and the new template share a single template: in Hot Glue’s case, it happens to be the _form partial.

This central partial is where both edit & new operations will be rendered from. In fact, it sort of is like a “show-edit-new” partial in that it can do any of the above.

Unlike Rails scaffolding of the past, there is no extra “show only” (non-editable state) template generated as I have found it not often useful. The _show partial displays a “show only” format of this record in list view.

You can specify field-level “show only” (non-editable) functionality— and you can easily make all of your fields “show only”.

However, there is no concept of the controller having a “show” state vs. a “edit” state. The fields that are show-only (non-editable) will display as non-editable on both the create & edit screen, and the controller will not allow those fields to be submitted.

A naive Rails approach to a “visible only” state would easily leave parameters you want to protect from hacking inside of the allow parameters list— a security flaw. If you’re a little fuzzy about when & where you want your users to view-only vs. be able to edit something, I’ve found this leads to quickly opening up field-level security flaws. For example, the user is allowed to have access to the record but they aren’t supposed to be able to update field X, but you accidentally leave field X in the allowed params list even though it doesn’t appear on the display. Hot Glue is encouraging you to think about the simple Rails code you are generating: it forces you to specify that this controller should have this access to these fields.

If you find yourself wanting to give different access in different cases, stop and listen very carefully: You probably want a new controller. One of the most common pitfalls of new Rails developers is Overloaded Controllers. Hot Glue is built to encourage you to keep making more controllers (even though at first it seems like too much code) when you want a new ‘feature’ or a new ‘function’ or a new ‘context’ or a new ‘path’ for that user for that object.

The basic architectural principal is based on Domain Context Interaction, which I recommend you read more about in Jame Coplien’s excellent book Lean Architecture. By building out many controllers you are encouraging yourself to see and identify the commonalities between them — those can be abstracted using mixes and inheritance — and discover the careful art of defining your business domain in conjunction with your code mental models.

Putting lots of operations upfront in the same controller is the enemy of this process, which is why Hot Glue encourages you to stick to the basic CRUD + list operations only for each controller and not make up your own actions.

There is one exception, and that exception is magic buttons, which are implemented as a sort of middle ground between making redundant controllers and overloading your controllers with business-specific actions. Magic buttons are buttons that submit as PATCH http verbs directly onto the existing update methods, which is correct & standard as according to the rules of REST, but instead of just updating data on the object they instead call a bang (!) method on that object. This bang method in turn invokes or effects some kind of non-idempotent (change-making) action (for example, Activate, Release, Publish, etc) for this context interaction. For more on this topic, see the discussion in Example 6 below.

Although it may seem like Hot Glue is generating too much code, remember that code is cheap and your time is expensive. The scaffold is designed to do what it needs to do today, for the given context it was built in, but be easily disposable and not get in your way to architect the rest of your app.

Routes File

Add these routes:

Rails.application.routes.draw do
  resources :books
  resources :authors

Does it work?

For this example, I’m using the like_los_gatos theme, which is a Netflix-inspired skin for Hot Glue that features bright Red buttons and inputs & textareas with 2px rounded corners. Be sure to install a theme in Step 6 when you install Hot Glue.

Go to http//localhost:3000/authors

It lets you create, edit and delete author records:

Now go to Go to http//localhost:3000/books

As you can see, out of the box Hot Glue produces views and a controller that is highly functional. In this one view, we’ve packed in a string, text field, float, integer, datetime, date, time, boolean, and enum.

Notice that for strings, Hot Glue made a simple one-line input field (like for name). For the long description, Hot Glue detected that the field on the database was type text and made the input a textarea box.

Hot Glue figured out the fields that were dates & times and datetime and output HTML5 date & time selectors.

Booleans magically appear as “Yes/No” radio buttons, and foreign keys (the author_id) and enumerated types are turned into drop-down lists automatically.

Because Rails default belongs_to also adds a validation to the field, the author field is required to be on the books above. When you go to save a book without selecting an author, the form is re-rendered with a red error message and the field where the problem is shown in red.

Example 1 App

Example 2: Authentication and Ownership

In this example app, I’ll cover how to setup Devise for authentication, and specify access control for your object using the logged-in user. To do this, you’ll want to set up a new app. This app is also very simple. It will contain only two tables: Users and Widgets.

A User has_many :widgets, and a Widget belongs_to :user.

Authentication, Authorization, and Access Control

Before we begin, let’s make sure we understand some basic web app terminology.

Authentication — The process where the computer (website) makes sure you are who you say you are.

Authorization — The idea that the user is allowed to access this website at all, or this part of the website.

Access Control — The idea that specific objects (like, objects in your database) are granted access (reading/writing) to some people and not to others.

Although these 3 terms of often confused, specifically because when we discuss these concepts in app development they are commingled, they have three distinct meanings.

Authentication is typically what we mean when we say “login.” By verifying the user’s username & password, we know that they are who they say they are. (At least, that they have the correct password.)

Authorization is often best thought of as a “public” part of your website which has no authorization (because any anonymous user can load the content) vs. an admin area where only admins are authorized to look at that part of the website.

If I have access to a part of the website, then the question becomes which objects I can access. In this example, users will log-in and create widgets. Another user, which we will simulate in an incognito window, will login and the 2nd user will not be able to see the 1st user’s widgets. The fact that the 1st and 2nd users can see only their own widgets and not each others is called access control.

Start with:

rails new WidgetsApp --database=postgres

(Please note that the currently released Devise is not compatible with the pre-release Rails 7 version. If you have Rails 7 installed, you will need to create a Rails 6.1 app using rails _6.1.4.1_ new WidgetsApp --database=postgres)

Now go through all Steps 1-9 in the Getting Started above, except Step 8 (Enum support), which we will not need for this example. After you install Devise in Step 9, there’s one final step that the setup docs leave out: You have to actually create a user and tell devise to know how to log that user in.

Remember, you’ve just run this command: rails generate devise:install (The last command in the Setup). That’s the Devise installer. Now you must create a user and also tell Devise to attach its fields (several fields are attached by default) to your user object. You can use any name for “user” you like— account, person, persona, customer, etc. This will be the primary way people will log-in, so think about the mental model of where you want your authentication to be.

Most apps use “user”; I often write my apps using the word “account.” In this example, I will use User.

Devise Setup

Let’s create the User table. The first migration will create the basic table. Be sure not to add the field email, which will be added by the second Devise migation.

From the Devise Rails setup, follow these steps:

rails generate model User first_name:string last_name:string

Notice we have a User model, a migration to create the users table, specs and a factory.

Then run:

rails generate devise User

Devise has created a second migration for the Users table,

Devise’s migration is quite large, and is worth looking at because it shows you the many great features that Devise comes without of the box. Note that every line below that begins with # is commented-out, meaning it won’t run when the migration runs. If you want the associated devise features (for example, to make your user accounts trackable, confirmable, or lockable, you’ll want to uncomment these lines now before running the migration. If you leave them commented, you can always create a migration later to add these feilds.)

# frozen_string_literal: true

class AddDeviseToUsers < ActiveRecord::Migration[6.1]
def self.up
change_table :users do |t|
## Database authenticatable
t.string :email, null: false, default: ""
t.string :encrypted_password, null: false, default: ""

## Recoverable
t.string :reset_password_token
t.datetime :reset_password_sent_at

## Rememberable
t.datetime :remember_created_at

## Trackable
# t.integer :sign_in_count, default: 0, null: false
# t.datetime :current_sign_in_at
# t.datetime :last_sign_in_at
# t.string :current_sign_in_ip
# t.string :last_sign_in_ip

## Confirmable
# t.string :confirmation_token
# t.datetime :confirmed_at
# t.datetime :confirmation_sent_at
# t.string :unconfirmed_email # Only if using reconfirmable

## Lockable
# t.integer :failed_attempts, default: 0, null: false # Only if lock strategy is :failed_attempts
# t.string :unlock_token # Only if unlock strategy is :email or :both
# t.datetime :locked_at

# Uncomment below if timestamps were not included in your original model.
# t.timestamps null: false

add_index :users, :email, unique: true
add_index :users, :reset_password_token, unique: true
# add_index :users, :confirmation_token, unique: true
# add_index :users, :unlock_token, unique: true

def self.down
# By default, we don't want to make any assumption about how to roll back a migration when your
# model already existed. Please edit below which fields you would like to remove in this migration.
raise ActiveRecord::IrreversibleMigration

Go to app/models/user.rb

Notice that devise has installed a macro onto the class which enables log-in/log-out functionality for this model. (As with the optional fields in the migration, the additional features are shown by default by the devise installer as commented out too.)

class User < ApplicationRecord
  # Include default devise modules. Others available are:
  # :confirmable, :lockable, :timeoutable, :trackable and :omniauthable
  devise :database_authenticatable, :registerable,
         :recoverable, :rememberable, :validatable

Let’s add some code for Hot Glue. Because Hot Glue needs any model to have a special field for its label, we’ll add a name method that will concatenate first & last names which are columns on the database. (If we had specified

The 5 magic names that Hot Glue will recognize are

 1) name, 2) to_label, 3) full_name, 4) display_name, or 5) email

If you have either a method name on your class (model object) OR a column name on your database table, Hot Glue will treat that as the label when displaying this record in drop-down lists and other places.

Remember, the order chosen is pre-determined. If you have both name and full_name, name will be used because it comes first in the list above. That means Hot Glue always prefers name first, and then to_label.

In this case, it turns out this step is not necessary if we had wanted to use email instead, Devise has installed it for us onto our User model in the 2nd migration.

Add the code shown in orange to the User model.

class User < ApplicationRecord
  # Include default devise modules. Others available are:
  # :confirmable, :lockable, :timeoutable, :trackable and :omniauthable
  devise :database_authenticatable, :registerable,
         :recoverable, :rememberable, :validatable
  def name 
    "#{first_name} #{last_name}"
  has_many :widgets

Now let’s create the Widgets.

rails generate model Widget user_id:integer name:string

Go to the app/models/widget.rb file and add a relationship to User.

class Widget < ApplicationRecord
  belongs_to :user

In this example app, we will not build any scaffolding for Users. Instead, Devise already comes with the sign up, log-in, and log-out screens we need.

Instead, we will simply log in and log out with Devise and create new widgets.

As well, our widget interface will be the only thing that happens. Open routes.rb and edit it like so

Rails.application.routes.draw do
  devise_for :users

  resources :widgets
  root to: redirect("/widgets")

Now, generate the Hot Glue scaffold:

rails generate hot_glue:scaffold widget 

Make to run rails db:create and rails db:migrate

Let’s see what happens when we go to https://localhost:3000/widgets

Even though I typed /widgets into my browser bar, this controller is protected by authentication and I’m not yet logged in. I get redirect to /users/sign_in and I see a message telling me I need to sign-in before continuing.

Remember, this functionality comes along with Devise — all of it has already been implemented for you.

Here I’m using the dark_knight theme provided by Hot Glue (see Step 6).

Since there is no users in the database yet, go ahead and use the Sign Up link.

(Alternatively, you can open the rails console and create a new user with User.create(email: "nonone@nowhere", password: "password", password_confirmation: "password") )

Now Devise will give you an ugly crash:

Geez that’s ugly! This happens because we need to define a special method on the ApplicationController to tell Devise where to send the user after they sign-up. Add this code to application_controller.rb

class ApplicationController < ActionController::Base
  def after_sign_in_path_for(resource)

IMPORTANT: Devise currently has serious compatibility issues with Turbo Rails. In particular, your log-in screens do not work out of the box. Follow the next step to fix them.

Manually port the Devise views into your app with

rails generate devise:views

Edit devise/registrations/new and devise/sessions/new, devise/passwords/new and devise/confirmations/new modifying all four templates like so:

form_for(resource, as: resource_name, url: session_path(resource_name) ) do |f|

change it to

form_for(resource, as: resource_name, html: {'data-turbo' => "false"}, url: session_path(resource_name) ) do |f|

This tells Devise to fall back to non-Turbo interaction for the log-in and registration. For the rest of the app, we will use Turbo Rails interactions.

Finally, add a Logout button to our application.html file (code shown in orange below).

<!DOCTYPE html>
    <meta name="viewport" content="width=device-width,initial-scale=1">
    <%= csrf_meta_tags %>
    <%= csp_meta_tag %>

    <%= stylesheet_link_tag 'application', media: 'all', 'data-turbolinks-track': 'reload' %>
    <%= javascript_pack_tag 'application', 'data-turbolinks-track': 'reload' %>

  <% if current_user %>
    <%= button_to "Log Out", destroy_user_session_path, method: :delete, 'data-turbo': false%>
  <% end %>
<%= render partial: 'layouts/flash_notices' %>

    <%= yield %>

Once you have completed the steps above, you should now be able to sign up, log in, and log-out without problems. (If you fail to follow the steps to disable Turbo Rails in the Devise views, you will encounter several issues with logging in and logging out.)

We can log in, log out. User 1 ( can create widgets owned only by them; User 2 ( can’t see User 1’s widgets and vice-versa.

And finally we see the power of Hot Glue’s authentication: Hot Glue has automatically detected that the Widgets belong to users, and it has installed access control on them for you. If your authentication model isn’t called User, you want to use Hot Glue’s --auth flag (see docs for details). But because we used the model name “User,” Hot Glue assumed this was how your visitors authenticated to the website. From there, Hot Glue has assumed that all Widgets are owned by the user who is logged in and given those users read, update, and delete access to them. Also, a special create action exists (at the proper place— the POST http verb of the /widgets endpoint) that will automatically set the user_id of the newly created widget to the current user’s id. You can see this at the top of the create action in the widgets_controller.rb.

def create
modified_params = modify_date_inputs_on_params(widget_params.dup.merge!(user: current_user ) , current_user)
@widget = Widget.create(modified_params)

Since this controller is intended to be used by users only, this makes sense. If you didn’t want the users to be able to make their own new widgets (for example, just to edit & update them), you would use the --no-create flag. Likewise, if you want to make a controller that doesn’t have delete use --no-delete. You can also use --no-edit when you want to not allow viewing/editing/update (although the LIST is still output). But note that if you use --no-edit along with --magic-buttons the magic buttons will override the no-edit, creating an update action used for the magic buttons only but there will be no ‘Edit’ button on the lines on the list view. (So from the user’s perspective, there is “no edit” button even though there are magic buttons.)

If you instead didn’t want this user to own this object in the context of this controller, then you’d have two options:

  1. use a --gd controller as in the Example 1.
  2. Implement your own access control

By default, when not creating –gd controllers, Hot Glue will impose this access control model on all objects. That is, to interact with the Widgets controller, the user must be logged in and own the widget itself (the user_id on the widget is set to the user’s id). For simple small apps, this is called “poor man’s authentication,” but it is not appropriate if you want granularity in access control (for example, contextual rules within the access control rules). For apps like these, I suggest you mix the --auth and --auth-identifier flags and implement your own access control methods. They can easily replace or augment the existing code, or live in the automatically created base_controller.rb which you will find created alongside every controller. (The examples on this page do not cover a non-standard access control implementation.)

Check out all of the generated code in app/controllers/widgets_controller.rb and make sure you understand it. The code in that controller behaves as described here. You should not use the glue blindly: instead, read the code, learn from it and work within Hot Glue’s lego-like structures. The structures are in place to encourage you to write secure, safe, fast, and performant apps but you will need to understand Rails when things get a little more complicated.

If Widgets had any child objects, those too would automatically be enforced with access control: Hot Glue would make sure that the child’s parent Widget belonged to the user who is logged in.

Example 2 App

Example 3: Namespacing & Nesting

In the last example I talked about how if you wanted to make a different authentication rules— for example, for a different part of your app— you should just make new controllers instead of overloading existing controllers with special business-level authentication rules.

In my fictitious scenario, we are operate a spa. Yes, like a spa where you can a Service like nails, hair, massage, scrubbing, bath, etc. Our services are stored in our Service table.

In our fictious spa the business owner needs an admin interface to create invoices.

In this example, we’ll make a classic Account has_many :invoices, and Invoice has_many :line_items.

Account will be the table where devise would operate— but we aren’t actually going to build the interfaces for the customer in this example. Account will serve as the primary means for the business to create transactions. However, the spa wants to keep track of several people within one Account, so there is a Person table that is a child to Account. (the Account has_many :persons). Note that the Account-Person relationship is the primary relationship, even though this special table (persons) has another relationship on the object graph.

The Lineitem represents the person who got the spa treatment and the date of the services, thus it has two fields: person_id (integer) and services_rendered_date (date)

Next, we’ll give lineitems a list of related Sittings, or “services provided.”

For an admin, we want to authenticate the admin up-front (by making them log-in), but once they do we we will give them access to a Gd controller. A Hot Glue God controller (unlike a “God object” in software) simply means it has no access control, so it can read & write any of the records for that table in the database.

However, unlike a normal billing system, the business owner asked for a special requirement: She wants there to be a “default price” for each service that is totaled along with the lineitem, but then she wants to be able to set special prices on the fly for some customers. In other words, as she creates invoices, she wants the system to tally up the price for the invoice, but then let her override that price on a per-lineitem basis. If she overrides that price, it should “stick.”

As well, for some reason she can’t quite explain, there’s a reason why she wants invoices with two levels of sub-lists: Lineitems, which show the visit and what the customer got charged for, and Sittings, which is a subordinate (child table) list to Lineitems that refers to the actual service that got provided (that is, you got X service on Y day at this specific time). I chose the word “Sittings” because the word “Services” is already used, and since both the customer and practitioner sit down for the service then the word “sitting” seemed like a good choice.

Example 3 App Entity Relationship Diagram: Account Person Invoice Lineitem Sitting Service

Another word for sitting might be “Service Provided” but that doesn’t fit nicely with Rails pluralization schemes. When you choose easily pluralized, single words, Hot Glue + Rails get along nicely. As well, try never to pick either database or field names that are Ruby keywords. This slightly extends the scope of where you can’t use Ruby keywords.

Remember, the Sitting will belong_to a :service, and the Sitting will also belong_to a :lineitem (so essentially Lineitems have_many services :through Sitting, which is possible to implement in Rails with this design but isn’t necessary to build the scaffold so isn’t shown here.) The total of the default prices for the sittings will be the “suggested” price for the Lineitem, but remember the business owner wants to be able to override this suggested price, inputing another a different Lineitem price on the fly.

Why the ERD is like this I can’t explain further— it’s a demonstration example. Maybe the Lineitem represents different days or different people, but for some reason this is the database design that works for this demonstration. (Please forgive that the entity design is somewhat strained for the example.)

This “sticky price” feature is NOT true for the Invoice, where the _total field should automatically and always sum up the price fields on the Lineitems— for this example, that’s a feature not a bug.

The Service table — which is global to everybody— holds only two fields: the name of the service and the “default price.” It contains records like Nails — $10, Hair — $20, Scrubbing — $15.

Start by going through all of the steps in the Getting Started on a new app For this example, we need both Step 8 (Enum) and 9.

rails _6.1.4.1_ new AdminInterface --database=postgresql

In this example, we’ll only concern ourself with the admin interface, so we’ll need a way to tell if the logged in user is an admin or not. Admins, who will have their own user record, will then browse a list of users. For each user record, the admin will be able to see all of that user’s invoices. On each invoice, the admin can see and edit each Line item.

Even though we will just generate admin scaffold, we’ll still reference a small part of what could be a bigger application. This user table will look like this:

rails generate model Account name:string is_admin:boolean

Then add Devise (Step 9)

  1. rails generate devise:install
  2. Add  root to: "home#index" to your routes.rb file
  3. Create a home_controller.rb
  4. Run rails generate devise account
  5. run rails generate devise:views and then fix Devise views as explained above.

Go ahead and do two more steps here

6. Create a file at views/home/index.erb

7. In views/layouts/application.html, add this button somewhere within the <body> tag:

  <% if current_account %>
    <%= button_to "Log Out", destroy_account_session_path, method: :delete, 'data-turbo': false%>
<% else %>
    <%= link_to "Log In", new_account_session_path,  'data-turbo': false %>
<% end %>

Notice that the current_account method is provided to us by Devise because we defined the Account object as where Devise does its authentication.

Even though we aren’t intending for customer to use this log-in, we still build it because it comes along free with Devise.

You app should be functional enough to sign up, sign in, and use the forgot password feature.

Then create the next 4 models:

rails generate model Person account_id:integer name:string
rails generate model Invoice account_id:integer number:string work_started:date _total:float paid_at:datetime
rails generate model Lineitem invoice_id:integer person_id:integer services_rendered_date:date price:float
rails generate model Sitting lineitem_id:integer service_id:integer notes:string
rails generate model Service name:string default_price:float

For this demo app I just threw an is_admin? flag on the User model which will be authenticated with Devise. For anything other than a demo app, I do not recommend this. The most popular Gems for granular user-level access control can be found on the User Authorization page of The Ruby Toolbox. (Pundit and Cancancan are the two leading contenders.)

Finally, take special care to note a subtle naming convention: The _total field on the Invoice begins with an underscore (_). Notice also that the other two places where amounts are stored: The price field on the Lineitem and the default_price on the Service, do not begin with an underscore (_). The underscore means Hot Glue will treat this field as “Show only” (or non-editable) by default. As well, we will use a small bit of Ruby magic to show how you can make the underscore field update when any of the child records are updated.

In our case, the underscore also communicates that this field is a field that is updated by the computer only, not by the user. That is, the should should not put data into the field and expect that data to persist. That’s why we’ve made the field ‘show only’ when building the scaffolding. As well, a field like this is set up to be used as a counter cache like Rails’ counter cache mechanism, but it turns out that Rails’ counter_cache doesn’t quite do what we need to do. To make the magic _total field update correctly, we’ll need to use a special implementation of counter_cache or a Ruby gem called magnusvk/counter_culture. I will show both implementations below.

Now let’s hook up our Active Record Relationships.

class Account < ApplicationRecord
  # Include default devise modules. Others available are:
  # :confirmable, :lockable, :timeoutable, :trackable and :omniauthable
  devise :database_authenticatable, :registerable,
         :recoverable, :rememberable, :validatable
  has_many :invoices
  has_many :persons
class Person < ApplicationRecord
  belongs_to :account
  has_many :lineitem
class Invoice < ApplicationRecord
  belongs_to :account
  has_many :lineitems
class Lineitem < ApplicationRecord
  belongs_to :invoice
  belongs_to :person
  has_many :sittings
class Sitting < ApplicationRecord
  belongs_to :lineitem
  belongs_to :service

And finally our “Service object” (which here just means “spa service” or “spa treatment” not actually “service” in web programming terms)

class Service < ApplicationRecord
  has_many :sittings

(If these relationships don’t make sense to you, refer to the diagram “Example 3 App Entity Relationship Diagram” above.)

Let’s talk about where these controllers are going to live. We want all of these controller for admins only, so the controllers & views will live inside of a namespace called admin. We will tell Hot Glue to use --namespace=admin and the controller and views will be in a folder called admin/

All of the generated Hot Glue examples in this section will be in this namespace. As well, they will all be God controllers. However, they will still use Rails routing namespaces and Rails nested routes.

Go ahead and add this route now:

Rails.application.routes.draw do
  namespace :admin do
    resources :services # at the top level of the namespace
    resources :accounts do # at the top level of the namespace
      resources :person
      resources :invoices do
        resources :lineitems do
          resources :sittings do
            # nothing here, you can leave this do block off if you want

  devise_for :accounts
  root to: "home#index"
  # For details on the DSL available within this file, see

Notice that services and accounts are both at the very top-level of the namespace; the others (invoices, lineitems, sittings) all are nested in a nest change beneath accounts.

That means, for Rails, when we load an invoice for editing, for example, the route looks like this:


The :account_id means the account id is part of this route’s URL, as is the invoice ID.

For the deeply nested object, the nesting gets even crazier:


Now let’s generate the scaffold for the accounts table:

rails generate hot_glue:scaffold account --god --namespace=admin

Note we are generating in the admin/ namespace, and also as a God controller.

After we generate the code, make one small customization to prevent yourself from deleting your own record.

Go to app/views/admin/accounts/_show.erb, scroll to the bottom of the file and add the unless block around the existing Delete button. Be sure to add the unless condition and the <% end %> block to tell Rails to only include the delete button if we aren’t looking at our own record.

This will be our only Hot Glue customization. If you were to re-generate the code again using Hot Glue, this customization would get wiped away, which I why I recommend these kinds of light, view-level customizations as “finishing touches.”

<div class="scaffold-cell scaffold-line-buttons" style="flex-basis: 150px;">
  <% unless account == current_account %>
    <%= form_with url: admin_account_path(account), html: {style: "display: inline-block;"}, method: :delete do |f| %>
      <%= f.submit "Delete".html_safe, data: {confirm: 'Are you sure?'}, class: "delete-account-button btn btn-primary btn-sm" %>
    <% end %>
  <% end %>
  <%= link_to "Edit <i class='fa fa-1x fa-list-alt'></i>".html_safe, edit_admin_account_path(account), disable_with: "Loading...", class: "edit-account-button btn btn-primary btn-sm" %>

For this example, I’m using the like_boostrap theme which is NOT Bootstrap but mimics the look & feel of bootstrap 4.

If we don’t add protection to these controllers, any user can read them, which you can see if you go to https://localhost:3000/admin/accounts/

Open up app/controllers/admin/base_controller.rb and add this code to protect all of the admin controllers collectively:

class Admin::BaseController < ApplicationController

  before_action :authenticate_current_account!

  def authenticate_current_account!
    if !current_account || !(current_account.is_admin?)
      flash[:error] = "Not authorized"
      redirect_to root_path

Now, if we go to, we get redirected to the home page. We even get redirected after we are logged in, because we didn’t assign the user with the is_admin. To do this, we will open the rails console and do this manually:

2.7.2 :002 > admin = Account.first
Account Load (0.8ms) SELECT "accounts".* FROM "accounts" ORDER BY "accounts"."id" ASC LIMIT $1 [["LIMIT", 1]]
=> # admin.is_admin = true
=> true
2.7.2 :004 >
Account Update (7.2ms) UPDATE "accounts" SET "is_admin" = $1, "updated_at" = $2 WHERE "accounts"."id" = $3 [["is_admin", true], ["updated_at", "2021-11-29 23:19:05.282899"], ["id", 1]]
=> true

(Type out the commands shown in red.)

It turns out we can’t actually add records in this way because Devise is enforcing that the password can’t be blank, and we have no way to enter password on these screens.

To work around this problem, create users on the Rails console using:

Account.create(email: "", password: "password", password_confirmation: "password")

Unfortunately this UX still has the problem that you can deprivilege yourself by removing your admin flag, and also you can delete yourself! Don’t do either one!

Let’s keep going and build the Person scaffold:

Remember, each Account has many :persons, and this is because there will be several people in a family visiting the spa at one time. The business wants the “account” to represent the person paying, and have several “persons” (or family members) able to get spa treatments under one bill.

Again, there are many alternative ways to implement a data model like this for such a business, but this data model is chosen as a demonstration of Hot Glue’s features.

However person is a child table to accounts, which you can see in our routes file. So we must tell Hot Glue that when bulding the person, the Person object is nested within account. Remember, we’re just talking about the relationships between these two tables on our six-table ERD:

By “nest,” what Hot Glue means is 1) the object is nested within this other thing on the object graph and 2) the routing structure is nested in the routes file.

rails generate hot_glue:scaffold person --nest=account --god --namespace=admin

We now get

       exist  app/views/people
      create  app/views/admin/persons/index.erb
        gsub  app/views/admin/persons/index.erb
      create  app/views/admin/persons/edit.erb
        gsub  app/views/admin/persons/edit.erb
      create  app/views/admin/persons/_form.erb
        gsub  app/views/admin/persons/_form.erb
      create  app/views/admin/persons/_line.erb
        gsub  app/views/admin/persons/_line.erb
      create  app/views/admin/persons/_list.erb
        gsub  app/views/admin/persons/_list.erb
      create  app/views/admin/persons/_show.erb
        gsub  app/views/admin/persons/_show.erb
      create  app/views/admin/persons/_errors.erb
        gsub  app/views/admin/persons/_errors.erb
      create  app/views/admin/persons/new.erb
        gsub  app/views/admin/persons/new.erb
      create  app/views/admin/persons/_new_form.erb
        gsub  app/views/admin/persons/_new_form.erb
      create  app/views/admin/persons/_new_button.erb
        gsub  app/views/admin/persons/_new_button.erb
      create  app/views/admin/persons/create.turbo_stream.erb
        gsub  app/views/admin/persons/create.turbo_stream.erb
      create  app/views/admin/persons/edit.turbo_stream.erb
        gsub  app/views/admin/persons/edit.turbo_stream.erb
      create  app/views/admin/persons/update.turbo_stream.erb
        gsub  app/views/admin/persons/update.turbo_stream.erb
      create  app/views/admin/persons/destroy.turbo_stream.erb
        gsub  app/views/admin/persons/destroy.turbo_stream.erb
      create  app/controllers/admin/persons_controller.rb
   skipping   base controller Admin::BaseController
      create  spec/system/admin/persons_behavior_spec.rb
   identical  app/views/admin/_errors.erb
% rails generate hot_glue:scaffold person --nest=account --god --namespace=admin
Running via Spring preloader in process 66380
      create  app/views/people
      create  app/views/admin/people/index.erb
        gsub  app/views/admin/people/index.erb
      create  app/views/admin/people/edit.erb
        gsub  app/views/admin/people/edit.erb
      create  app/views/admin/people/_form.erb
        gsub  app/views/admin/people/_form.erb
      create  app/views/admin/people/_line.erb
        gsub  app/views/admin/people/_line.erb
      create  app/views/admin/people/_list.erb
        gsub  app/views/admin/people/_list.erb
      create  app/views/admin/people/_show.erb
        gsub  app/views/admin/people/_show.erb
      create  app/views/admin/people/_errors.erb
        gsub  app/views/admin/people/_errors.erb
      create  app/views/admin/people/new.erb
        gsub  app/views/admin/people/new.erb
      create  app/views/admin/people/_new_form.erb
        gsub  app/views/admin/people/_new_form.erb
      create  app/views/admin/people/_new_button.erb
        gsub  app/views/admin/people/_new_button.erb
      create  app/views/admin/people/create.turbo_stream.erb
        gsub  app/views/admin/people/create.turbo_stream.erb
      create  app/views/admin/people/edit.turbo_stream.erb
        gsub  app/views/admin/people/edit.turbo_stream.erb
      create  app/views/admin/people/update.turbo_stream.erb
        gsub  app/views/admin/people/update.turbo_stream.erb
      create  app/views/admin/people/destroy.turbo_stream.erb
        gsub  app/views/admin/people/destroy.turbo_stream.erb
      create  app/controllers/admin/persons_controller.rb
   skipping   base controller Admin::BaseController
      create  spec/system/admin/persons_behavior_spec.rb
   identical  app/views/admin/_errors.erb
@ /Users/jason/Work/Hot_Glue/Example Apps/AdminInterface [main]

Here’s something a little funky to note about the current state of your app: You have a working Persons controller, and it behaves as a subordinate controller to Account. This means that it expects to always operate as a nested resource. You can work with Persons only if you hack the URLs, like so:

Assuming you know the account ID of an account is 4, you can see the associated persons records at:

This screen lets you create a new person as you would expect from normal scaffolding:

If you happen to know the ID of the record you just created, you can access it via the default scaffold:

WARNING: This is for demonstration only. These nested paths, although they are exposed to the end-user, are not intended to be loaded like this. Like the old Rails scaffolding, if you do this you can wind up in some strange places, but everything works according to the standard RESTful resources you have built with the scaffolding:

However, the great flexibility of Hot Glue is that Uis can be composed together from the perspective of the top-level of your namespace.

If you use --nest alone, you must manually stitch your views together after generating them.

Hot Glue has a trick called downnesting that will automatically create portals to your related records.

We will now regenerate the Account scaffold adding a --downnest flag.

rails generate hot_glue:scaffold account --god --namespace=admin --downnest=persons

Here, we are telling Hot Glue to generate a scaffold for the table accounts, do it with God mode so that the controller can read any record, make this controller and its views inside of the namespace admin/, and create a downnested portal to the persons relationship.

Here I’ve changed the theme from Dark-to-White but it is still the same output.

Remember, Hot Glue build all of this for you with just two commands:

rails generate hot_glue:scaffold account --god --namespace=admin --downnest=persons


rails generate hot_glue:scaffold person --nest=account --god --namespace=admin

You get a fully functional screen where you can add persons record portal — or list of related records — from each account record. When you add new person records, they are automatically put in the right account.

Notice that the Delete & Edit buttons off to the right are for the account table (to delete account records). It’s easy to visually confuse them with the Delete & Edit buttons for each row in the portal (to delete persons). Some of the Hot Glue themes address this by making the buttons get smaller as they are nested within the portal row, but obviously this is just a starting place for your UX.

Here you can edit the Account names to be the family names of the spa’s guests: “Smith Family” and “Maple Family.” The persons records are created within each account. Notice that the first record is the admin record so we should not edit that record.

Multiple-Level Downnesting

Remember that we’ve only added two of our six-table data model for our Spa example. Accounts also have Invoices, which will operate in parallel to Persons (both are direct child tables of Account). Nested within Invoices we have more nested: Lineitems, and nested within Lineitems we have Sittings.

Now we’ll build out the interfaces for this part of the object graph:

We’ll build Invoice, Lineitem, and Sitting together. Notice that Sitting does not downnest anything, because it is the lowest table on the tree, there’s no further down the tree to go. However, Sitting is nested by three tables: account/invoice/lineitem. This format with slashes (/) indicates a hierarchal relationship between the models.

Lineitem has a downnest (sitting), and it also is nested by two parent tables (account/invoice).

Invoice has a downnest (lineitem), and it also is nested by account.

We already built the account scaffold, but here we will add another downnest to that scaffold, so it will have downnesting for both persons (as we built above) and invoices.

Before we can work with these tables, we need a few modifications. Go to app/models/sitting.rb and add this code

class Sitting < ApplicationRecord
  belongs_to :lineitem
  belongs_to :service

  def to_label

Here we’re telling Hot Glue that when to display a Sitting record, use the related service’s name.

Also add this to lineitem.rb

class Lineitem < ApplicationRecord
  belongs_to :invoice
  belongs_to :person
  has_many :sittings
  def to_label 
    "for #{} on #{services_rendered_date}"

rails generate hot_glue:scaffold sitting --nest=account/invoice/lineitem --god --namespace=admin

rails generate hot_glue:scaffold lineitem --nest=account/invoice --god --namespace=admin --downnest=sittings

rails generate hot_glue:scaffold invoice --nest=account --god --namespace=admin --downnest=lineitems --exclude=total,paid_at

Remember, the nested by (or –nest flag) setting should always match both your Rails routes and your object graph. Remember that our Rails routes have all of invoices, lineitems, and sittings nested (each within the previous) within the accounts resource block, which is at the admin namespace:

Review the routes.rb file to keep this in mind:

namespace :admin do
  resources :services 

  resources :accounts do 
    resources :persons

    resources :invoices do
      resources :lineitems do
        resources :sittings do
          # nothing here, you can leave this do block off if you want

Ok, here you go. It’s isn’t the most intuitive interface with 3-levels of nesting, but as you can see it comes out of the box fully functional:

Example 3 App

Example 4: Validation


This example is simpler than the last. We’ll create a Gd controller (an admin controller) for just one table: Events.

rails generate model event name:string start_at:datetime end_at:datetime promote_on:datetime

Our system system is going to enforce these rules:

• The end date can’t be before the start_date.

• Our promotion system, which will make our event public, can’t have a promote_on date that’s after the start_date

If you aren’t familiar with doing this in Rails, head on over to the Rails Guide for Validations and familiarize yourself with that now.

class Event < ApplicationRecord
  validate_presence_of :name
  validate :start_at_before_end_at, if: -> {!start_at.nil? && !end_at.nil?}
  validate :promote_on_before_start_at, if: -> {!promote_on.nil? && !start_at.nil?}

  def start_at_before_end_at
    if end_at < start_at
      errors.add(:start_at, "can't be after end at")
      errors.add(:end_at, "can't be before start at")

  def promote_on_before_start_at
    if start_at < promote_on
      errors.add(:promote_on, "can't be after start at")

Since Hot Glue detects validation at run-time and not at code generation time (like relationships and field definitions), you can add the validations before or after you generate your code.

Example 5: Magic Buttons

Magic buttons are cheating overloaded controllers, which is an antipattern. Instead of adding another ‘action’ on our controller, we want to perform some non-standard update operation. But the non-standard thing happens on its own, off the LIST page from the user’s perspective (not the edit page), and its form submission shares the update action on the Controller.

Personally, I like list views that have multiple buttons:


To me, updating the actual data on the record is probably the less common user operation than the meat & potatoes of your app: Activating, releasing, canceling, etc.

Magic buttons let you simply add buttons on your list view that will submit to your update action on the controller. From there, the update action will pick up the presence of a special (non-field) flag on the form submission. A little Ruby magic is used to make sure that these special flags aren’t part of the update call on the object, but they will trigger operations on the object with the same name plus a bang (!). For example, if you click magic button Activate, it will submit to the controller like so:

The controller will detect the flag activate in the params, even though there is no such field on the object called activate.. It will then call the activate! method on the object and automatically suppress activate from being part of the ActiveRecord update call (so as not to try to save a field by the same name). To be clear, your magic buttons must not be the same name as your field names. Although sometimes I may have something similar (“activate” is the magic button name and “activated_at” is the field name that indicates when the person got activated), they are like actions you can perform on the objects and should not clash with the names of fields on your models.

You shouldn’t abuse this to put too much business logic in the controllers. You should do this only lightly and only in the context of “doing something update-like” on the object. If you are “doing something update-like,” it’s OK to use magic buttons like this. However, if you find yourself writing a lot of business logic in the controller, instead abstract that logic out. As explained in Example 1, you probably want another controller. From there, you can use mixins and inheritance (or other patterns) between the controllers to share logic.

You should also not take this as an invitation to open the door to putting too much business logic on the models, either. Those sneaky ! methods on your model object probably shouldn’t be very large— if they are, consider writing business objects outside of your MVC layer and using your controller only to orchestrate business objects & database objects only. For me, anything beyond very simple database-like operations on my models get abstracted out into business operations. For that, you will want to replace Hot Glue’s default code with your own business operation code, passing in any parameters & objects that business operation object needs to do its job.

The magic button construction — which admittedly is probably borderline antipattern itself— is designed for starter apps and lightweight starter code. Be careful to avoid the common pitfalls of Rails developers explained above. Balancing the need for speed and lightweight solutions with good application design will get you very far here and Hot Glue is designed to be your training wheels only. What I’m saying here is that the magic button implementation does correctly encourage you to move your business logic out of the controller— but stuffing it over into the models using a bang method, although often very quick & convenient, isn’t necessarily the right solution for your app.


Localized Timezone Support

In order to show dates, we use a localized date display: the date & time is always shown to the user in their own date. To do this you have a few choices: (1) You can save the timezone to the user’s table, and let them set it for themselves, (2) You can show everybody the server’s date.

Option #1 – Store timezone On the User object

We already took care of this by adding timezone to our User object.

Option #2 – Use the Server’s Timezone

If the auth object (current_user) does not respond to timezone, the Rails “system clock” will be used. The system clock’s timezone as set by the Rails app is used. This is often the timezone of the headquarter’s of the company that owns the application. (That is, if you do not know the user’s context, you simply use your own company’s context instead.)

Power of Hot Glue

Hot Glue harnesses the power of many great things about Rails:

• Turbo-Rails for fast front-end interactions

Database migrations

ActiveRecord associations (has_many, belongs_to, etc

• Chains of has many relationships for simple “starfish” access control

Devise for authentication

Bootstrap & FontAwesome to tie things together and make them look slick.

Remember, make your models first: Add limits and defaults to your database fields by modifying your migrations. Then add the relationships between the tables using standard ActiveRecord has_many, belongs_to, and has_one.

Then build the common core scaffolding & customize the views and controllers it produces.

With these powerful tools, you can build a dashboard-like app in minutes, complete with simple interface buttons that let your users accomplish most of what they’ll need. The philosophy is that you will want this dashboard as you initially introduce people to your product. The main logic of your application will likely live more in the models, service objects, and domain layer (business logic) parts of your Rails app. For this reason, you are encouraged to customize the files only lightly. (Add some verbiage or change the CSS to customize the look & feel.)

The code you build with common core is cheap and disposable. It is not very modern, but it gets the job done. It is just “good enough” to launch a sophisticated app on, but it isn’t good enough to impress your users with a really good UI.

For that, you’ll want to throw away the front-end code and replace it with a modern JS UI like React, Vue, Ember, or Angular.

By that time, Hot Glue will have already helped you build a prototype and your business logic. For admin-only screens where a full-featured dashboard is desired, building scaffold using Hot Glue is a perfect fit.