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 template, right?

Not so fast. The Digest represents a point-in-time snapshot of the state of the world. If you’re a developer, your mind is probably jumping to the dreaded place we all loath: timetime zones.

If the digest is supposed to go out at 4:00 pm, I had to ensure that each user will receive it at 4:00 pm in their time zone.

I decided to lean on Postgres to compute the hour of the day for each user based on the time_zone stored on each record. For example, this clause will return the numeric hour of the day at the time the query is executed, in the US Central Time Zone:

EXTRACT(HOUR FROM NOW() AT TIME ZONE 'America/Chicago')

 Tracking state

I debated how much state to persist in the database for each generated digest.

One option is to go completely stateless: when the time is right to send the digest, just fetch the data, render it, send off the email, and call it a day.

This approach is naive. If the sending phase happens to fail and needs to be reattempted later, you would either need to temporarily persist the contents of the email somewhere (or hold it in memory and pray the host stays alive).

This approach is also hard to make idempotent. If we aren’t tracking whether a user has already received their daily digest, then the task would need to rely heavily on running at a specific time of day, or else you risk generating a digest for the same user multiple times in successive runs of the generator task.

I decided instead to save a record in a digests table for each generated digest, with a key column that has a uniqueness constraint:

CREATE TABLE digests (
    space_user_id uuid NOT NULL,
    key text NOT NULL,
    -- ...
);

CREATE UNIQUE INDEX digests_space_user_id_key_index ON digests USING btree (space_user_id, key); 

Digest keys must be unique for each day (e.g., daily:2018-11-16). This guarantees that a user will never have two daily digests generated on the same day.

I’m also storing other metadata (subject line, date range, sections, posts in each section, etc.) so that I can provide a permalink to view the digest in the browser.

 Designing the email

Designing HTML emails sucks. You probably have a slight advantage if you designed websites back in the 90s, when you had to use <table> tags for EVERYTHING and you only used CSS to remove the underline from hyperlinks (admit it…that was your first exposure to CSS too 😜).

I spent about a day hating my life trying to get this damn thing to look good in email clients, but I’m pretty happy with the result:

image

Some tips:

⇨ I used Postmark’s fabulous transactional email templates as a starting point

⇨ I tried to use utility CSS classes as much as possible. In my opinion, it’s especially valuable to think in utilities for designing emails because you have to be very careful about only using CSS that plays nicely with email clients and utility classes communicate very clearly what CSS rules are in play.

⇨ I used premailex for inlining CSS styles before sending. This is an absolutely essential step. Different languages have different libraries for accomplishing this, with varying degrees of maturity. It feels a bit like the wild west in the CSS inlining space, unfortunately.

That’s all for this update! As always, feel free to check out the source on GitHub:

⭐️ Email layout
⭐️ Digest view

 
20
Kudos
 
20
Kudos

Now read this

How to display server-rendered HTML in Elm 0.19

I’m building a team communication product called Level, and one of the core features is rendering posts as Markdown. Elm has a library to handle this on the client-side. It’s a wrapper around the marked.js library that uses Kernel code... Continue →