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

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:

  1. Register the module to the Mar.Route.
  2. Save information in the %MyApp{} struct.
  3. 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

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:

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:

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

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

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

The hx-target attribute takes CSS selectors like hx-target="#main".

HTMX has hx-swap options:

hx-trigger sets the event type with default values for

Some other examples:

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

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-confirm gives 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 for keyup only 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.

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.

Stems

Each element has a Flow-in version and a Flow-out version.

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.

Bases

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.

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.

Aspects

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.

Again, the translation is my own interpretation.

Phases

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.

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

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