The Level Journal

Stories about building my startup.

Read this first

Level is coming in January

A few weeks ago, I started testing Level with a small group of early access customers. I have to admit, I was pretty scared. I’ve been toiling away for months, creating something from nothing, hoping that it would resonate with people.

I wasn’t quite sure what to expect. Would anyone actually like it?

Over 6,000 people have expressed dissatisfaction with the status quo. But are people actually willing to do the hard work of switching to a different tool, in pursuit of a better workflow?

Of course, it’s impossible to know for sure with a tiny sample size…but I saw a glimmer of hope.

I watched a small team fully embrace Level and leave behind their push-notification-driven workflow. For the first time outside of the laboratory, I observed Level doing the exact job I designed it to do. I was floored.

Seeing this team succeed with Level inspired me to take another scary leap, which I...

Continue reading →

The Daily Digest

Level is intentionally designed to leave you alone. This is great for productivity, but there’s a careful balance to strike to make sure that people don’t forget about periodically checking in on their messages.

The first step toward closing this gap is the Daily Digest.

Planning the feature

I had some initial criteria. The Digest must:

⇨ Include a summary of unread and read posts in the inbox
⇨ Include a sampling of posts/replies, formatted similarly to the app
⇨ Have a clear call-to-action
⇨ Send at a predictable time

There are a lot more exciting possibilities for the digest email (such as intelligently summarizing other “activity” that may be of interest to the user), but I decided to start with the most essential parts to keep scope manageable.

Dealing with time

This feature seemed pretty simple at first. All I have to do is query the database and render some data in an email...

Continue reading →

Full-text search with Postgres

This week I built very basic search capabilities into Level.

In the past, I’ve used Elasticsearch. I had some hesitations:

⇨ Synchronizing data stores is a pain
⇨ It’s another piece of infrastructure to administrate
⇨ It makes it harder and more expensive for someone to download and run their own copy of Level (if they care about production-level reliability)

I decided to try out Postgres’ built-in text search features and found it to be a pleasant experience!

Here are a few resources that helped me:

⇨ The Official Manual - a well-written tutorial, worth a read through
⇨ Postgres full-text search is Good Enough!
⇨ Mastering PostgreSQL Tools: Full-Text Search and Phrase Search

Continue reading →

Building file uploads

Last week, I predominantly spent my time implementing file uploads. This was a tricker feature implement on the frontend because Elm does not have good support right now for dealing with file objects. It turned out pretty slick!


Here were my essential requirements:

⇒ Users should be able to drag-and-drop files and paste files from the clipboard onto any post or reply composer
⇒ Users should see a list of attached files on posts
⇒ Images should be embedded, and files should be hyperlinked at the current cursor position in the editor

The backend

One of my favorite characteristics of the Elixir ecosystem (and perhaps the functional paradigm in general) is the lack of “magic” that plagues the Ruby world. In general, I’ve become pretty skeptical about adding unnecessary dependencies to Level and maintain a pretty high threshold before bringing in any outside packages.

I found Arc when...

Continue reading →

Moving to source-available licensing

I’ve made the decision to update Level’s licensing to a source-availability scheme. This is something I’ve been giving careful thought over past few months as I evaluate my goals and my vision for the product. Ultimately, I believe it is the most sustainable way to license the codebase and will allow me to continue fearlessly building it out in the open.

A quick tour of licensing

There are several common licensing schemes (and accompanying business models) for software businesses:

  • Closed-source and proprietary
  • The “open core” model: an open-source core version with a dual-licensed proprietary offering (e.g., GitLab)
  • Open source with a managed services component (e.g., Discourse and Ghost)
  • Source-available (similar to “open source” but with some added restrictions, such as the Commons Clause)

A majority choose to adopt the closed-source, proprietary model. The calculus is pretty...

Continue reading →

I finally learned some OTP

It took just over a year of working with Elixir before I found the need to learn more about Tasks, GenServers, Supervisors, etc. Last night, I spoke at the local Elixir meetup about my first major feature leveraging this technology.

Here are the slides!

View →

Building push notifications

I spent a few days while traveling to Seattle for ElixirConf working on push notifications. I was unfamiliar with the technologies required to send OS notifications from a browser-based application, so there was a bit of a learning curve.

Getting push notifications to work was pretty gnarly, to say the least. Here’s what Google’s guide on the Web Push Protocol has to say about it:

One of the pain points when working with web push is that triggering a push message is extremely “fiddly” […] The main issue with triggering push is that if you hit a problem, it’s difficult to diagnose the issue. This is improving with time and wider browser support, but it’s far from easy.


I plan to do a full write-up on how to accomplish this (using Elixir), but here’s the gist of how it works for...

Continue reading →

Why Phoenix.LiveView is a big deal

Last week at ElixirConf, Chris McCord announced a new project called Phoenix.LiveView. I believe this library has the potential to reshape the way many developers build reactive user interfaces on the web.

Today, Phoenix provides a mechanism for defining Elixir modules that represent different views in your application. View modules contain a special render function that returns the rendered output for the particular route (typically HTML) and any other helper functions you need for rendering.

LiveView is an extension of standard Phoenix views that will automatically propagate updates to the browser anytime state changes that would impact that view (using the existing Phoenix channels infrastructure).

Instead of sending a state change event down the wire that you have to process in JavaScript manually, LiveView sends down an updated HTML fragment, diffs it against the current DOM...

Continue reading →

My first day at ElixirConf

I flew into Seattle yesterday, one day before ElixirConf kicked off. I spent the day hitting up my favorite spots for coffee and strolling around the city. It was a productive day writing code and scribbling down ideas in my notebook.

Today began with a keynote from Jose Valim, the creator of Elixir. I came away from the talk encouraged about the future of the language (not that I was concerned before). He spoke about what to expect in future releases and stressed the criteria that the core team puts on any newly proposed additions to the core language.

Although Elixir is a relatively new language, it is quite stable. Elm’s development trajectory is similar.

As I’ve matured as a developer, my appreciation for stability has grown. One of the reasons I’m so skeptical about investing in any particular JavaScript ecosystem is the sheer volume of churn. This is becoming a tired complaint...

Continue reading →

Tracking inbox state

Today I worked on the logic for tracking the state of posts in a user’s inbox. I’ve abstained from jumping too quickly into the implementation phase, to avoid painting myself into a corner and potentially having to rewind data architecture decisions prematurely.

It’s worth mentioning a few articles that are helping guide my thinking:

→ Lee Byron’s post architecting a Facebook-like activity feed
→ The definition of event sourcing and various related articles floating around the internet

Some questions I’ve been pondering:

→ How should prioritization work?
→ Should posts ever automatically be dismissed when read?
→ How should I represent the “reason” a post is in the inbox?
→ Should I use fanout on read or write to assemble the inbox?
→ How will Level prevent inbox bloat for the user?

I’ll admit, I’ve felt a bit paralyzed by the scope of all these decisions the last few weeks. Rather...

Continue reading →