- 2024 CE
- Worked on creator apps to consolidate society, technology and my personality. Created an astrology app while heightening my technical ability.
- Mar — Simple Web in Elixir
- Creator apps
- Hypermedia Systems
- Modern divination
- Building Pillars
Mar — Simple Web in Elixir
Mar is a simple web framework written in Elixir functional programming language. The design of the library aims to provide a Flask-like experience for Elixir developers. It is a layer of abstraction on top of Plug powered by Bandit.
Design
The design goal for Mar is to make it transparent to the user so someone who just covered the basics of the language can jump right into building a web application. Elixir offers a powerful and aesthetic syntax built-in, such as multi-clause function definitions with parameter pattern matching, module with struct definition, @behaviour and Protocol for polymorphism, and use macro that injects code into the module. Mar attempts to fully utilise these native features to provide a simple and intuitive API for building web applications.
For the user, such brevity means typing in use Mar makes all the connections and sets up reasonable defaults so a web application is defined without an extra file in the lib/. With object-oriented programming, it’s as simple as passing an object. That’s what we see from Flask, Sinatra or Express. The object can encapsulate the state and operation of the application without too much ceremony on the user’s codespace. But for the functional programming paradigm, it usually spills out to the user modules. To manage this, Mar has to keep as much as possible in the library. And it takes accessing the module information of the user code from the library.
I was surprised by the level of frustration such an approach can cause. At first, I thought it was just me who couldn’t come up with an elegant solution right away. But as I dug deeper, I found the trail of lamentation. Most libraries resort to taking some configuration for a module, so the user can manually report to the library. Other than that, the ambition of the community has reached scanning all the modules in the user project and saving them in an ETS or an Agent. I tried it and it was a convoluted, error-prone, and hard-to-maintain solution, at least for my technical capability.
Protocol is an often underappreciated feature of the language. And it sheds light on the problem. Protocols do module consolidation in order to enhance the performance. And the protocol can reach the consolidated modules with __protocol__(:impls). If Mar can slip in protocol implementation code into the user module with use Mar, it will automatically report the module to the protocol. And Mar can access the module without extra configuration. This comes with a nice side effect. Since defimpl takes structs anyway, saving information in the struct is a natural way to keep the module information. And the protocol can work as a liminal codespace instead of being a tool for module registration.
So that’s exactly the way Mar is implemented. Each user module is a unique implementation of Mar.Route. It provides some default defimpl functions and Mar.__using__/1 injects them alongside defstruct handling options. This way, use Mar makes these things possible:
- Register the module to the
Mar.Route. - Save information in the
%MyApp{}struct. - Interact with the library through
Mar.Route.MyApp
use Mar takes path and params options. path is the URL path of the route where path parameters are annotated with :. They are parsed and appended to the existing params.
Mar also exports Mar.child_spec/1 so the user can start the supervision tree just by adding Mar as a child. The child spec starts Bandit with Mar.Plug as a general pipeline which goes down to Mar.Router where path-matching, parameter parsing, and action dispatching are done.
Path-matching algorithm is far from optimised. It’s a few iterations over the list of routes. Stream is applied where applicable to alleviate the performance. Once the path finds the module of responsibility, path parameters are parsed to the Plug.Conn first, where they merge over query parameters and body parameters. Then parameters are selectively loaded to the struct. The HTTP method is loaded as well for the action name. Then before the action is dispatched, conn is loaded to the struct. This allows Mar.Route to expose the entire connection to the user module. After the action is dispatched, it exposes the connection once more. And finally, the response gets sent.
Installation
Create a project with supervision tree:
$ mix new my_app --sup
$ cd my_app
Add Mar to your dependencies in:
# mix.exs
defp deps do
[
{:mar, "~> 0.2.0"}
]
end
Add Mar as a child:
# lib/my_app/application.ex
def start(_type, _args) do
children = [
Mar
]
# ...
end
Add Mar to your module to make it a route:
# lib/my_app/route.ex
defmodule MyApp.Page do
use Mar
def get() do
"Hello, world!"
end
end
Spin up a server:
$ mix run --no-halt
$ curl localhost:4000
# Hello, world!
Use
Using Mar makes the module a route.
It injects Mar.Route protocol and a struct.
Set a path or the default is “/”.
Add parameters in the path or in the params list.
defmodule MyApp do
use Mar, path: "/post/:id", params: [:likes, :comment]
end
Name your function after the HTTP method. Take HTTP parameters from a map. Return a response body with text.
def get(%{id: id}) do
"You are reading #{id}"
end
Returning a map will send a response in JSON.
def post(%{id: id, comment: comment}) do
%{
id: id,
comment: comment
}
end
Return a tuple to set HTTP status and headers.
def delete(%{id: _id}) do
# {status, header, body}
{301, [location: "/"], nil}
end
Mar.Route protocol lets you access Plug.Conn.
defimpl Mar.Route do
# Mar.Route.MyApp
def before_action(route) do
# Access `route.conn` before the actions you have defined.
route
end
def after_action(route) do
# Access `route.conn` after the actions you have defined.
route
end
end
Creator apps
Last summer, I took a retreat at a town in Seoul known for its guesthouses. There were little stores around the corners, unique cafés, and Michelin-confirmed restaurants. The creative vibe of the town gave me a chance to settle down and contemplate.
For years, I’ve been trying to understand the essence of social progress. The history of life, the journey of mankind, the motives of modernisation. I was able to cover the bare minimum to see what and where we are.
But it was difficult to answer as to what I should do at this moment. It’s clear that we should start weaving a social system based on data and creative collaborations, instead of possession and transactions. But what does such a transition take? Can you name one? Am I in the right place to practice it? Or am I just an observer contemplating on what’s possible but can’t be done yet.
One thing that I’ve been practising was app-building. I’ve been learning and curating the most effective ways to build user-facing web apps because I thought that’s the closest thing I can do for the future society. I thought I might systemise a new social network that captures mutual recognition in data, not money.
But the activity of the network still remains as a question. Though I have ideas: creating, crediting, and recognising, they are way too abstract to convey the nuance. If you’ve told a medieval man that commerce and finance would change the world, you would have been laughed at, then executed. There had to be the process of transition such as the Renaissance and the Reformation. And such massive transitions can’t be foreseen until they happen.
The retreat was an escape for me from such an analytical perspective. It was moving to discover the unique projects and their attempts to survive this commercial society. They might have been commodified in a more typical setting. But the locality of the town made all the difference. Each of them had a personality that makes it personal to me as well. I noticed that the charm touches the softer sides of my mind. I was inspired from the human touch.
Then something clicked. I wanted to build apps for the ones who have created the moving experience. As a matter of fact, the personalities behind the projects have changed what an app-building means to me. Ever since then, I have been working on a technical foundation that can serialise such lightweight apps with its unique creative context.
I’ll discuss the technical side in another post. In this one, let’s talk about what I mean by creator apps and how creative collaboration can enable them.
Creators of change
The creator economy has become a thing only after the rise of social media. Namely, creators are content creators and the contents they create fuel the social media.
Is that all? The companies behind the social media often get the spotlight because they are the enablers and they are huge. Indeed, giant platforms such as YouTube, Instagram, or TikTok seem to define what it’s like to live this time.
But I want to bring the focus back to the creators. There’s a fundamental human nature underneath the production and consumption of the creator economy. While social media have innovated the delivery, the point is what is delivered.
And I believe the parcel, the human nature, is change. The creators create, after all, changes. The human mind vigilantly seeks change. Some do so to follow it, some others to fear it. Even though the discussions around social media tend to care more about how dopamine works, if you zoom out, what they make is a new socio-political layer that enables change.
What’s interesting is the quality of these changes. So far, human society has sought changes that are rational: more, better, stronger. But this new, postmodern mode of creation makes things simply different. In this new culture, the change creators don’t necessarily improve something. They simply share what they mix, improvise and explore.
This is possible because of The Zero Marginal Cost Society. It could have been costly if we didn’t have infrastructures like the web. Since virtually anything can take a digital form and travel to the other side of the globe as a matter of a second, the burden of change is as heavy as air.
Applications of change
Of course, it makes the process of creation highly reliant on the digital technologies. The social media apps are answers to this problem and they are already deeply seated in our daily lives. Not just the media, countless apps have emerged in the last decade to aid the creators so they can navigate the digital environment.
The apps have one thing in common: they are utility-first. Each of them has a vertical where it solves a specific problem. This approach is great when you need smooth user experience for a functionality. YouTube has unparalleled video delivery. Instagram understands what it’s like to upload media from a mobile device.
But the world of software development has grown. There are so many, perhaps an overwhelming number of tools, resources, and providers that facilitate app-building. Small teams, or even an individual can deliver apps that make impact now.
What if an app can be made for a change? rather than a utility. Again, this change doesn’t have to be something righteous. That kind of missions belong to an ideology or a religion. This change is something unique to each of the change-creators.
Entrepreneurial creators already have prepared useful apps under their belts. But the typical software development takes a whole team to manage. Otherwise, the creator has to outsource the app with all the risks on their side, while some others wrestle with no-code tools, if not Wordpress.
Creative collaboration for creator apps
My suggestion is a new model for software development: creative collaboration. Tech start-up founders struggle to find a vertical. Creators struggle to adapt their creative context into the digital world. They can solve each other’s problems. The creator becomes the founder’s vertical, and the founder takes the creator’s vision to the digital world.
The creative vertical is powerful because it’s unique. There’s no real competition. The creator already has an audience to try the app. UX issues can be minimised as it’s highly customised for the specific use cases for the audience.
Quite the contrary, the whole experience of the app can be accepted as a form of communication with the creator. Even the troubles along the way gets a chance to be forgiven because they are from the creator, not some machine glitch.
This new approach has been my personal inspiration. As a kid, I always wanted to create something on the web. While my childhood didn’t work out that way, building on the web still gives me joy.
My motivation with websites was in the expressiveness of them, rather than the technicality. Then I went to art school, received an education in fine art. I was looking forward to work with creative people using the technology.
And I can feel I’m there now. The creative apps idea has become my central theme. I’m not sure how this idea will unfold and transform. There’s only one way to find out.
Hypermedia Systems
Foreword
Nelson describes a future where the barriers to publishing and data sharing are lowered and the creative energies of the world are easily shared and applied. This is neither a new nor unique idea but one that does need continual renewal and encouragement.
It sounds like there was an expectation for hypermedia to be some kind of world system then it didn’t.
Nelson saw his hyperlink and hypermedia as the driving force for intertwingularity between people and machines around the world. In this idea alone, hypermedia is a powerful approach to creating computer systems that enable people to work together for the common good.
There is a trace of social philosophy in this.
Introduction
And then, of course, there are hypermedia servers, which present hypermedia APIs (yes, APIs) to clients over the network.
This API notion attempts to set the scene.
On typical AJAX single page applications:
Applications built in this style are not hypermedia-driven: they do not take advantage of the underlying hypermedia system of the web.
REST ≠ JSON. REST simply means browsers exchanging hypermedia.
Hypermedia-Driven Applications, or HDAs
The idea of hypermedia systems is curtained behind React.js + Node.js apps.
We’re writing HTML not just for a particular application, but also to play along with other members of the web.
HTML is hypermedia-friendly when it is written for the full range of constituents of the hypermedia system.
What are these members/constituents?
1. Hypermedia: a Reintroduction
- Hypermedia: media that branches to other media, Hyper-: line of escape beyond
- Hypermedia Control: Elements of interactions in hypermedia like hyperlinks. This is what makes media hyper
AJAX requests for JSON are not a hypermedia control because it asks for some data, not hypermedia. Now the client has to know about this arbitrary set of data resulting in tight coupling. Where, I suppose, hypermedia already knows how to represent itself.
I’ve got it. Hypermedia is media. It’s meant to be represented somehow. SPAs appropriate a part of HTTP to exchange pure data without any representation. Then it cobbles up its own show.
The controls stop driving the interactions and devolve into mere UI elements that talk to the JS engine.
So, hypermedia had a dream. Hypertext didn’t meet it. SPAs abused JS. And now, HTMX brings back what hypermedia could have been.
I’m glad Svelte was introduced, even though it’s “unsatisfactory”.
Hypermedia-driven applications can be SPA or MPA. It’s not about the pages. It’s about putting hypermedia at its core.
- Hypermedia-Driven Application (HDA)
- A web application that uses hypermedia and hypermedia exchanges as its primary mechanism for communicating with a server.
Now I get the context. HTMX states the interaction and the representation on the markup. You know what you are asking for and where you will see it.
<button hx-get="/contacts/1" hx-target="#contact-ui">
Fetch Contact
</button>
Unlike a JS function, the code clearly states, and has to state that you will see /contract/1 on #contact-ui.
2. Components of a Hypermedia System
It’s pretty obvious now. But location, and cache-control were not intuitive at first. today I learn about vary.
vary designates headers that should get cached content varied. When the denoted headers have different values, a cache has to consider it a new version even if the rest of the request is the same. If the header values keep changing, caching doesn’t matter. vary: * signals the same.
Uniform interface is an important quality of hypermedia system. A hypermedia client is supposed to know what it gets. JSON APIs deviate from this.
So, the components are:
- The hypermedia with hypermedia controls: HTML
- A protocol to handle the hypermedia: HTTP
- Any software that implements the protocol: web servers
- A technology that represents the hypermedia properly: web browsers
REST
REpresentational State Transfer is a network architecture. The name means sending formatted data somewhere. The RE part is an emphasis that the data should be presented in a form and not just some mathematical values. It makes the data heavier, but eradicates the need for managing processing engines everywhere.
REST has 6 constraints. And most of them are implemented on the network level:
client-serverstatelesslayeredcaching
I’ve been curious about the necessity of sessions. And it’s been said that an ideal REST would be completely stateless without a session. I agree. Sessions always seemed fragile. What does it mean to authentication?
One surprising lesson is the importance of caching. I thought caching is some hacky way to cut costs. But it’s actually a required quality for REST.
The concerns of a web application are:
- Uniform interface
- Code-on-demand
Uniform interface is implemented as HTML. And within it, Code-on-demand is implemented as JS. The problem is that the turing-completeness of JS has taken over the place of HTML. Code-on-demand is eroding uniform interface.
Uniform interface
- Identification of resources: URL
- Manipulation of resources through representations: HTTP methods
- Self-descriptive messages: HTML semantics
- Hypermedia as the engine of application state: HTML forms, navigations, and HTMX
HATEOAS or the hypermedia constraint bundles the three preceding constraints and completes the uniform interface constraint
In a web browser. It means that HTML contains the forms that send HTTP requests that performs methods on URLs.
- Question
- As innovative as it sounds, how does a template in a web framework can be an engine of app state? It sounds peripheral, or a component at best, like a view in MVC. The server still needs to implement a function corresponding to each URL
The hypermedia constraint emphasises that the user should do what they are allowed on the page, interacting with links, forms, and whatever elements enchanted with HTMX.
Now, the problem is how the web development framework can ensure such design by default.
3. A Web 1.0 Application
/contacts
/contacts/37
/contacts/new
Phoenix does this too. But I don’t like that new. It looks arbitrary. What else then? It should probably be included in contacts. HTML tags, like a toggled modal, should provide further user experience, not navigation. And the HTTP methods can discern what comes next, not the URLs.
I see patterns I’ve seen in Phoenix. The error handling is similar. And I don’t believe users can make an error. Only the program does. This Post/Redirect/Get pattern, or the PRG pattern is a legacy of Web 1.0.
Indeed people debate on the URL design for new. Silly. edit is another one.
- Tip
- Note that factoring on the server-side tends to be coarser-grained than on the client-side: you tend to split out common sections rather than create lots of individual components. This has benefits (it tends to be simple) as well as drawbacks (it is not nearly as isolated as client-side components).
So, the web 1.0 app shows a pattern with native HTML forms. In the absence of other methods, the forms POST it to utility paths like /new, /edit, or /delete
4. Extending HTML as Hypermedia
HTMX answers to these opportunities of HTML
- Any element to be a hypermedia control
- Any event to trigger HTTP request
- Any of CRUD to be the HTTP method
- Any part of HTML to represent the HTTP response
Partial HTML interpolation has a legit foundation:
Ted Nelson, in his 1980 book “Literary Machines” coined the term transclusion to capture this idea: the inclusion of content into an existing document via a hypermedia reference.
There are five methods
hx-gethx-posthx-puthx-patchhx-delete
The hx-target attribute takes CSS selectors like hx-target="#main".
HTMX has hx-swap options:
innerHTMLdefaultouterHTMLbeforebeginafterbeginbeforeendafterenddeletenone
hx-trigger sets the event type with default values for
input/testarea/select:hx-trigger="change"form:hx-trigger="submit"- all others:
hx-trigger="click"
Some other examples:
hx-trigger="mouseenter"hx-trigger="click, keyup"
It can filter inputs. hx-trigger="keyup[ctrlKey && key == 'l']" will react to ^L. It can take any JS function that returns boolean.
hx-trigger="click, keyup[ctrlKey && key == 'l'] from:body"
hx-trigger has several directives of which from: that delegates event listening.
Htmx: HTML eXtended
That sounds like “HTMX” to me.
- Serialising forms
- When an htmx-powered element is within an ancestor form tag, all input values within that form will be submitted for non-
GETrequests.
If not a form entry, hx-include attribute can bring a user input to the server. hx-include="#search" will find and includes #search.
If you don’t want to generate IDs, use relative CSS selectors: closest, next, previous, find, this. hx-include="closest form" will find the closest ancestor form. next and previous scans sibllings in their directions. find scans the children. this is, you know, this.
hx-vals can include hard-coded values. Or, with js: directive, you can get it evaluated in JS.
hx-push-url simulates browser history. But this only works if the action URL is different from the current one. If you want browser history getting /contacts in /contacts, incorporate a full-page response.
5. Htmx Patterns
AJAX-ifying Our Application
hx-boost="true" turns a and form act like SPA for free, replacing just the body so head can rest. The server can just send the full HTML page as usual. HTMX is smart enough to ignore what’s not necessary. And the navigation updates as usual. Everything will work the normal way.
Boosting can prevent flash of unstyled content.
HTMX attributes cascade. hx-boost="true" applies to the children as well, which can be reversed with hx-boost="false".
This is an example of progressive enhancement where typical users enjoy the boon yet still works for the ones who have JS turned off.
A Second Step: Deleting Contacts With HTTP DELETE
Flask defines 302 as the default redirection. Isn’t that weird? Not 300, not 303 as in the book, but 302?
Anyway, it makes sense that 303 is appropriate when you want to show the index page after deleting an item unless you have a special page for deletion.
- One More Thing…
hx-confirmgives you a simple dialog.
Next Steps: Validating Contact Emails
Add /contacts//email just for validation?
- Debouncing Our Validation Requests
hx-trigger="change, keyup delay:200ms"debounces for 0.2 second.- Ignoring Non-Mutating Keys
hx-trigger="change, keyup delay:200ms changed"triggers forkeyuponly when the content is changed, ignoring non-mutating keys.- Infinite Scroll
hx-trigger="revealed"turns it into an infinite scroll page. Wow.
Modern divination
Would you believe that you were born with 4 pairs of letters and they can tell how your life is going? Well, I didn’t until I decided to make a whole app about it. Check it out if you already want to know your Pillars.

My partner goes “So I looked up their Pillars…” whenever she meets someone. The Four Pillars of Destiny is an astrology widely active around the Far East. In the ancient days, people would depend on these letters to understand and, hopefully, predict their lives.
While being ancient, the Four Pillars still count. They’re brought up to resolve relationship issues. Birth rate bumps when it’s the year of Dragon, like this year. There are many other applications: Feng Shui, Gua, Qi Men Dun Jia, and so on.
Do they make any sense though? I remained disengaged, skeptical, and sometimes critical because I thought such superstition would hurt good judgement.
Then I saw my partner raising hopes in her most difficult times. More often than not, there are not much good judgement can do. That’s when I thought I had to give the astrology thing a second thought.
Pillars helps you uncover the stories of your life with a modern take on the ancient astrology.
I still don’t think the Pillars predict or diagnose anything. They are purely fictitious. Then what are fictions for? Once you accept that a story is just a story, it becomes a source of wisdom and inspiration.
It’s been said that people tend to agree random fortune telling. The Barnum-Forer effect exposes how, not dumb, but complex we are. Any story can work as a mirror that reflects yourself within yourself.
The Pillars make a great story-generator. The four pairs of ten and twelve letters produce 604 = 12,960,000 possible combinations of Pillars. Each pair gets assigned a relationship that opens up space for interpretation.
I thought it would be fun to build upon this idea. That’s how I started working on Pillars
Brutalist graphics and Ayu Dark
My main goal was to provide intuitive graphics and an interface. The result is the brutalist style and mobile-first interface you can see in the app.
At first, I thought the graphics would be emojis because they are the most intuitive graphics of our time. And they are easy to implement. But as I work on this idea, I soon realised that it’s difficult to add layers and convey enough information on top of emojis.
So I popped the bubbles and started sketching custom graphics. I wanted them to visualise what they stand for, but in an abstract manner. This is exactly what the original letters do. Every character comes from an image, a hieroglyph. Then it becomes a symbol that can mean a lot of things that doesn’t have to be the original image.
I had to do this because I was replacing all the letters with the graphics. The letters are the foundation of the Pillars. If I’m replacing them away, it had to be something profound. I thought it’s a good application for a brutalist design. Not for the sake of trend, but for the function. I’m not disappointed with the result.
I had an idea for the colours too. In my previous project, Ayu for Obsidian, Ayu Dark was under-utilised. I had it in my mind and brought to this project.
With the brutalist design and Ayu Dark, I was confident to proceed. I’ll write about technical challenges in another post. The following is more about the content of the app.
The Sexagenary Cycle
Before I go into app building, I had to know how the Four Pillars work. I had basic understanding from the long, cultural exposure. The bottom line is that there are 60 Pillars, of which you have four. But writing an algorithm was going to take more than that.
As I expand my knowledge about the horoscope, I also wanted to provide a new, good translation. Most resources available online put them in alphabet and that’s it. That’s not very intuitive if you don’t know the letters they refer to.
陰陽, or yinyang, is the foundation of the Pillars. While it refers to two things, Yin and Yang, it’s important to note that they are one thing. They don’t mean anything without each other. Then what is this one thing?
Any living system has its inside and outside. The surface between in and out defines what it is and what it’s not. Interesting movements happen on the perimeter. The system takes things in: food, light, and knowledge. And it becomes what it hasn’t been. As much as that, things go out: vigour, action, and words. Then it forgoes what it has been.
I reckoned that yinyang closely resembles such flow of life energy. Yin shelters life under its shade, while Yang exudes energy to raise it. So I decided to call them Flow-in and Flow-out. They add dynamism to the translation. It’s also a nice coincidence that Yin and in sound the same.
Then there are five elements: 木 Wood, 火 Fire, 土 Earth, 金 Metal, and 水 Water. It seems these things stood out in many ancient cultures. Metal is the only addition compared to the four classical elements. It’s also the most complex in the letter form. Perhaps it comes from the level of progress the ancient society achieved at the time.

Each element has a Flow-in version and a Flow-out version.
- Wood: 甲 Tree — 乙 Vine
- Fire: 丙 Blaze — 丁 Ember
- Earth: 戊 Land — 己 Yard
- Metal: 庚 Ore — 辛 Gem
- Water: 壬 Lake — 癸 Rain
These ten symbols are 十天干, or 10 celestial stems. They make the body of your Pillars. They have been used as ordinal numbers in the ancient times. Just like A, B, C, it goes Tree, Vine, Blaze, and so on. There are ten probably because early humans counted with their ten fingers.
The translation is unique to this project. I carefully reflected what they represent in the Pillars. Also I matched the number of letters between the two versions.

The well-known animal zodiac signs, 十二運星, are the manifestations of the celestial stems. They are called the 12 terrestrial bases, basis for singular. They make the bottom lines of your Pillars.
They also worked as ordinal numbers when the ancient people had to count twelve. Sailors and astronomers would have been most interested in their utility. Every other basis shares a flow state just like the stems. But the elements are assigned with a more spatial way, making it difficult to write down in text.
- Wood: 寅 Tiger — 卯 Rabbit
- Fire: 午 Horse — 巳 Snake
- Earth: 辰 Dragon / 戌 Dog — 丑 Ox / 未 Ram
- Metal: 申 Monkey — 酉 Rooster
- Water: 子 Mouse — 亥 Boar
Counting them doesn’t start from a Wood symbol this time. Tiger comes to the third, just like the season starts in March. Astronomically accurate start of the year is the darkest winter. Mouse symbol represents that moment of beginning which is also considered to have transitional energy.
You can see that Earth takes the center and has twice the members compared to other symbols. In the framework of Pillars, you will often find it as a central and neutral point of reference.
And that’s how you get 60 variants of Pillars! If you start counting one stem and one basis at a time, it takes 60 steps to get back to the initial pair, because the least common multiple between 10 and 12 is 60.
The relationships among the cycle
With the basics cleared, we can go into the juicy part.
Elements within the Celestial Stems interact with one another and create a relationship. If you pick any two, one element either 生 bears or 克 eats the other. For example, Wood burns into Fire, but it consumes Earth. Water can grow Tree, and Metal cuts the Vine.

These interactions come into 十神, or 10 divine aspects. They also have flow variants depending on if the two elements have the same state or not.
- The elements are the same: 比肩 Match — 劫財 Contest
- The element bears the other: 食神 Heritage — 傷官 Challenge
- The element eats the other: 偏財 Exploit — 正財 Property
- The element is born by the other: 偏印 Project — 正印 Institute
- The element is eaten by the other: 偏官 Discipline — 正官 Authority
Again, the translation is my own interpretation.

Last but not least, 十二運星 or 12 vital phases are the relationship between the stem and the basis in a Pillar. The phases are about making a lifecycle from birth to death: Origin, Nurture, Vigour, Renewal, Attire, Mission, Apex, Wane, Malady, Demise, Respite, and Verge.
- Stem is Earth: Cycle starts in Water Bases
- Stem is Metal: Cycle starts in Wood Bases
- Stem is Water: Cycle starts in Fire Bases
- Stem is Wood: Cycle starts in Metal Bases
- Stem is Fire: Cycle starts in Water Bases
And if the Stem is Flow-in, the cycle goes backwards.
The lessons, readings, and what’s beyond
The most beautiful part of the research and development was the lunisolar calendar. It’s the foundation of the Pillars. And it has definitely made the Gregorian calendar look bad even though it’s the globally accepted one. Lunisolar calendar actually reflects how heavenly bodies operate and the mother nature’s cycles. It starts with the winter solstice and peaks at the summer solstice. In the meantime, Gregorian calendar
I’ve also encountered a fascinating diversity. Even if the year starts at the winter solstice in the cycle, some practices applied the new year only after Spring season began. While the divine aspects are about between stems, some references applied them to bases as well. They would even flip some bases’ flow state.
Authoritative practitioners called for precision and legitimacy. Compassionate ones were focusing on making positive impact. Both approaches make sense. While working on the app, I weeded out vulgarised practices and kept the astronomical accuracy for the most foundational. At the same time, I didn’t compromise user experience just for the sake of precision.
Another surprise was that, coming from antiquity, some parts of Pillars tend to stereotype genders and family roles in a traditional way. I could see how they had to be concealed for Pillars to survive in our modern society. I decided to completely remove those parts from my app to keep it modern and non-binary.
Those decisions were applied to the algorithm, the design, and the prompt. You can check your Pillars and find out what they mean in Pillars app. You can get a reading currently at €8. The app is not complete in any sense yet. But I’m hoping to expand on features and reusability with daily, yearly, and relationship readings.
Enjoy!
Building Pillars
For the last dozen weeks, I’ve been working on the foundation for creator apps and an example: Pillars. This is a brief report for my choices and what has been done.
Since the plan is to serialise lightweight apps, the main objectives of the foundation were to
- setup light and flexible environment for creator apps
- centralise common and heavy parts to the core app
- test and confirm the stack with an example app
Elixir, HTMX, and Tailwind
I feel so lucky that Elixir is my environment to work in. It’s often considered a niche technology for CTOs to consider for specific situations. But I believe it’s the best kept secret among indie makers too. It’s just so magically simple and beautiful.
# Working example of Plug by José Valim the creator of Elixir
Mix.install([:bandit])
Bandit.start_link(plug: fn conn, _opts ->
Plug.Conn.send_resp(conn, 200, "hello world")
end)
Flexibility and maintainability are some of the most powerful qualities of Elixir among many others. Elixir has a Rails-like web framework called Phoenix. But I wanted more brevity than robustness for my creative apps. So I decided to go with the Elixir.Plug interface.
Elixir supports umbrella apps. An umbrella app can have multiple apps in it that share configurations and dependencies. This feature is not exactly the community’s favourite. But it seemed I could make a good use case. So I adopted it. I’m also looking forward to provide a lighter web framework for the ecosystem. Mar was an experiment to showcase the idea.
Leaving the JS ecosystem has implications. And I’ve decided to cover them with HTMX. It’s a JS library to end all JS libraries. Web apps built with HTMX extend the idea of Hypertext as the Engine of Application State. Instead of communicating in JSON to translate them into a view with obscure logic, HTMX expects some hypertext to replace the current view.
<!-- Replaces the button with whatever /clicked responds with -->
<script src="https://unpkg.com/htmx.org@2.0.3"></script>
<button hx-post="/clicked">
Click Me
</button>
The HTMX library is full of utilities that I haven’t fully explored yet. It basically has everything you need to build a web app from any back-end environment. But for me, it’s the philosophy behind what REST and hypertext really are. Learn more about it at Hypermedia Systems and Fielding, 2000
HTMX contains logic neatly into the markup. And you guessed what I’d do about styles: TailwindCSS. I loved the utility-first approach since my first encounter. It has grown significantly and became a necessity at this point. Tailwind aims for larger teams. But even for small teams and individuals, the simple fact that you don’t have to name anything yourself talks to me.
In a conjunction with HTMX, this is a great selling point. HTML has pre-defined, semantic tags. This is what the Web should be: a global protocol, that is not perfect but everyone can share an idea and generally agree upon. CSS and JS has derailed this quality and overcomplicated web development.
With Tailwind, HTMX, and later-adopted Elixir.Temple templating engine, everything is neatly sorted into the templates. And the only thing I need to name is the Elixir functions which are lovely to work with.
The passkeys
Once I set the environment up, I moved onto the core app that centralises some common needs such as authentication. At first, I thought I would use a provider for logging users in and out. But further research made me turn my head to the FIDO2 compliant web authentication API also known for its passkeys.
The web didn’t have an elaborate authentication system for a long time. The basic authentication is, you know, too basic. So the developers had to roll their own auths using web forms.
And that’s by large why authentication on the web is so brittle. It’s error-prone and easy to hack. People forget their passwords while some others give theirs away in a phishing site. It’s a total mess. This kind of engineering problem takes a whole team of experts to deal with. And that’s exactly why I wanted to use external providers.
// Prompt the user to sign up
const publicKey = {
user: { id, name, displayName: name },
rp: { name },
challenge,
pubKeyCredParams: [es256, ed25519, rsa256],
}
const credential = await navigator.credentials.create({ publicKey })
But the web authentication API changes the game. It delegates authentication to the device that already has powerful authentication features like facial recognition. A group of experts from FIDO Alliance writes the standard and the World Wide Web Consortium takes care of the spec. The API is ready for use on your favourite browsers today.
It’s one of the most secure authentication methods the web has ever had. But the real reason to adopt this new feature is the user experience native to the device. It actually feels secure for the user and removes all the unnecessary intermediaries. It’s just the cool way to sign in.
There’s a catch though. The spec is rough around the edges now and lacks some features. In particular, the user.name property is decoupled from the web apps, but somehow is encouraged to be managed server-side. If that sounds weird, you’ve read right. Users can change the name arbitrarily and the server wouldn’t know. Then how does the app manage it? Proposals are in process to fix this issue but in the meantime, I decided to dub them with the app name.
Plus, the Instagram/Facebook in-app browsers, or WebView implementations, don’t support the API. I don’t know why. 🙄 I had to escape those things.
// iOS Chrome
location = "googlechrome://" + location.href.replace("https://", "")
// iOS Safari
location = "x-safari-" + location.href
// Android
location = "intent:" + location.href + "Intent;end"
Once logged in, the user account session is stored in a cookie that can be accessed from all subdomains. Since I’m going to publish the creator apps under the subdomain, the user authenticates only once in the core app.
Wrapping up
Rest of the development was a breeze. The lunisolar calendar was concluded with 200 lines of tests and 270 lines of logic. The app went live once it was connected to PostHog, Stripe, and Anthropic. Req, by the way, is a fantastic Elixir HTTP client that is handling the providers. Its interface actually feels Elixir. I’ll write a separate post about its excellent design and the functional interface of Elixir.
Despite the joy of app building, I had to abruptly wrap it up for several reasons.
- I wanted to move onto evangelising creator apps
- Now I have a better idea about the UI design
- And a better idea about the project structure
I handed off the current state of the project to my future self with 13 new GitHub issues. I’ve been coding for more than 3 months non-stop. I like the result and learnt a lot from the process. It has been confirmed that a single person can deliver something people pay for.