Header logo.
small hallucinations
homeyearstagsaboutrss

Streaks Are for Boring Things

I was studying the UX design of Apple Books and noticed that at the bottom of the Home screen, there is a reading goal progress dial, followed by your current streak, and your longest streak. You keep the streak alive if you spend enough time reading in the Apple Books app to meet your goal.

This streak design is not really a source of bragging rights, as there isn't much of a social element in Apple Books. There's no “my streak is longer than my frenemy's” badge of honor for readers to display.

At the same time, Duolingo's design has combined streaks with social features to an extreme. You have your own streak. When you break your streak, your friend can save it for you by spending their own gems. When you pledge to maintain a streak of a certain length and fulfill the pledge, you get extra gems. You are also constantly reminded that your friend has achieved a remarkably long streak. All this adds to the social pressure on users to keep coming back to the game. (Duolingo has taken gamification to another level. It would be disrespectful not to call it a game.)

Now, what's interesting is that you only want to use the streak feature for activities that are traditionally considered boring or unengaging (reading, learning vocabulary).

If something is inherently engaging, you don't need a “streak” feature to remind users to come back. For example, you don't see a “streak” reminder splashed across TikTok's home screen.

Building a Landing Page with Codex, Astro, and Cloudflare

I created a landing page for a project that I've wanted to start for a long time. Much of the delay came from my struggle to wrap my head around it.

The best way to beat procrastination is to do something—anything, really—and simply get started. For me, that “something” was building a landing page.

I asked Codex to generate five landing-page mockups in vastly different styles. I then refined the design by asking it to create variants of the two I liked best.

As of August 2026, Codex (with gpt-5.6-sol) understands images remarkably well. It can generate design mockups and accurately interpret their layouts.

After some discussion, I asked Codex to build the site with server-side rendering so that users would receive fully rendered HTML without much JavaScript fluff. I also wanted the landing page to include a waitlist feature.

This was my first time using Astro. Judging by the code Codex produced, Astro handles the job cleanly and elegantly.

What's remarkable is that Astro creates a server-side endpoint that receives signup requests without adding any bloat. The endpoint then inserts each email address into a database table.

Cloudflare makes this especially simple. I deployed the landing page to Cloudflare Pages and connected it to a D1 database to store incoming waitlist signups. It just worked.

A lightweight Swift workflow with Zed

I played with Swift over the weekend, and Xcode felt sluggish at times on my machine. I also had to ask Codex for help quite often just to navigate its UI.

When I tried coding in Zed, it failed to recognize structs, variables, and similar symbols defined in other project files.

A quick Google search led me to a very helpful blog post, “How I Got Zed Editor Working with Swift Projects,” and a GitHub repository: xcode-build-server.

Following the instructions in the blog post, I installed xcode-build-server and generated a buildServer.json file with the following command:

xcode-build-server config -scheme SCHEME_NAME -project PROJECT_NAME.xcodeproj

Zed could now recognize the project's symbols.

I wanted to use the simulator without opening Xcode. After some trial and error, I got hot reload working too. Here's how:

  • Navigate to the project directory and run open -a Simulator.
  • Run xcodebuild to build the codebase as an .app bundle.
  • Run xcrun to install and launch the built bundle in the simulator.

I've shared the Bash script I'm using in a GitHub Gist. Update the project name, scheme, directories, and other values as needed, then install fswatch to watch for file changes. Now you can run this command and have fun:

open -a Simulator && dev.sh

Your ideas are wrong (and your instincts aren't)

I've been reading Life at the Speed of Play by Mark Pincus, the founder of Zynga, the social gaming company behind FarmVille.

Here are a few takeaways that I found fascinating:

  1. Instincts and ideas are two different things. Your instincts are almost always right; your ideas are often wrong. Recognizing this distinction makes it much easier to abandon failing ideas without giving up on the core insight. Your instinct tells you where the market is going or which problems are worth solving. Your ideas represent specific ways of building a product or finding a solution. Ideas tend to be wrong because they involve countless permutations of details. Which brings us to:
  2. Proven, better, new. When you follow an instinct, find the features that are proven to work in competing products and copy them. Identify which details in existing products you can objectively make better. Introducing new features or ways of interacting is risky. That's why:
  3. A “minimum idea state” is much more useful than a “minimum viable product.” MVPs are too heavy. Instead, build just enough of your idea to rapidly gather data and test the waters.

Alpaca WebSocket Authentication with WebSockex

I was trying to stream market data through Alpaca's WebSocket endpoint.

Alpaca's documentation lists the following three steps for receiving market data from the stream:

  1. Connect to the endpoint.
  2. Send an authentication frame.
  3. Send a subscription frame.

Step 2 proved tricky.

The library I'm using is WebSockex. As a best practice, you should either send the auth frame in the handle_connect function or reply with one from handle_frame when the “connected” message is received. In this particular case, both approaches failed or timed out.

The handle_connect function is called immediately after the WebSocket connection is established. From handle_connect, you cannot return {:reply, auth_frame, state}; you can only return {:ok, state} or {:close, state}.

If you want to send the auth frame to the server, you have to use WebSockex.send_frame(pid, frame). ChatGPT keeps telling me to do the following, although it invariably causes a CallingSelfError. Apparently, you cannot simply ask yourself—the same process—to send a frame.

1def handle_connect(_conn, state) do
2  # This will cause an error.
3  WebSockex.send_frame(self(), {:text, auth_frame})
4  {:ok, state}
5end

But in the handle_frame function, you can return a tuple like this: {:reply, auth_frame, state}. You only need to pattern-match the incoming frames. When you see “connected,” return that reply tuple. This would have worked had it not timed out. (Curiously, replying with a subscription frame worked.)

In the end, I sent the auth frame inside the start_link function.

 1  def start_link(state) do
 2    connected = WebSockex.start_link(@uri, __MODULE__, state)
 3
 4    case connected do
 5      {:ok, pid} ->
 6        WebSockex.send_frame(pid, {:text, auth_json})
 7        {:ok, pid}
 8
 9      {:error, reason} ->
10        Logger.info(reason)
11    end
12  end

Building Complexity from Simple Ideas

I finished reading Elixir in Action. It's a great book that explains complex ideas—many of them new to me—in clear, simple language.

In each chapter, the author illustrates a set of new concepts with simple code examples and occasional exercises. As you go deeper into the book, you can clearly see how new concepts are built on the simpler ones introduced earlier. It feels recursive.

At its core, Elixir is very, very simple. You define small functions that perform specific tasks. You can specify the conditions under which these functions run using the when clause and parameter pattern matching. You then build larger functions by composing smaller ones.

The notions of pure functions and immutability go hand in hand. They may seem like constraints at first, until you realize that they free you from having to ask yourself, “Am I manipulating this list in place or not?” or “Is this pointer mutable or not?” No, you didn't change the list in place. No, it's not mutable, and you don't need to worry about pointers.

The most basic way to coordinate processes is by sending messages. A process exits after it receives and processes a message. To keep a process alive, you recursively call the function that receives messages. There are complex features, but these simple concepts remain at the core. This makes Elixir much easier for me to think about than goroutines and channels.

Go and read it.

Two Ways to Learn with a Coding Agent

  1. Ask your coding agent to generate a minimal, runnable code snippet that demonstrates a concept.

  2. After vibe coding, ask your coding agent to summarize its changes and create Anki flashcards.