Changelog recently open sourced the Phoenix application that provides the backend for their website and podcast publishing platform. They published an announcement post along with the source code on Github.

This was a good oppurtunity for me to take a look at someone else’s Phoenix application, to see how they do things. Below you can find the notes I made while looking through the source.

The Basics

  • Phoenix 1.2
  • Elixir 1.3
  • Ecto 3.0
  • Webpack 2.0

Dependencies

The following dependencies were ones I didn’t recognise or found interesting:

Routes

  • Separate admin and public pipelines.
  • admin pipeline sets layout, requires admin.
  • auth pipeline looks for current user.
  • public pipeline uses LoadPodcasts plug to pre-load podcasts.
  • Most paths are served by PageController, which loads a template based on the action name.
  • /weekly and /nightly routes are for mailing list subscriptions.
  • Sub-paths for subscribe, confirm, unsubscribe, etc.

Lib

  • ConCache is a worker child of the ChangeLog application.
  • /uploads directory is setup to be served statically for dev only in Endpoint.
  • /lib/changelog/enums.ex defines a custom column type for the podcast status. This could be used in multiple models.
  • /lib/changelog/factory.ex is a collection of factories for the various models, using ex_machina.
  • /lib/changelog/hashid.ex sets up the encode/decode for the hashed IDs. Salt also set here.
  • Commonly used regular expressions are defined in a Regexp module.
  • Scrivener default page size is setup in Repo.
  • Page <title> set via Meta.Title and Meta.AdminTitle. Imported in LayoutView.
  • Other metadata (twitter, images, feeds, description) set by additional Meta modules.
  • craisin is the name of the application that handles Campaign Monitor access. Is based on HTTPoison with multiple resources as sub-modules. All mailing list alterations done over API, no local DB records for this.

Models

  • Channels are not currently exposed in the public interface. They are playlists for episodes from different podcasts.
  • Not all the models are Ecto backed. Newsletter for example.
  • Use of Ecto.get_change to delete records where the delete key has been set in a changeset. Pattern is shown in Ecto.Changeset documentation. Uses a virtual delete field to remove nested records. Untested. (1)
  • Episode.with_numbered_slug is used to scope episodes using a regex.
  • Episode slug is a string. Episodes with numeric-only slugs are “numbered episodes”. Episodes with any non-numeric characters are bonus content.
  • Episode.extract_duration_seconds uses FFMPEG via System.cmd to get the episode duration.

Controllers

  • There is a lot of application logic in the controllers. (2)
  • scrub_params is used to covert empty strings in changeset values to nil.
  • Slack integration for listing upcoming episodes with a countdown.

Helpers

  • ViewHelpers.no_widowed_words prevents an overflowed string from only having a single word on the final line. Inserts a &nbsp; between the last two words.
  • Viewhelpers.tweet_url has multiple defs for tweet_url, with a default AND a guard. Header w. default, header w. guard, other headers as usual.

File Uploads

  • Uses Arc.
  • Uploadables defined in web/uploaders/
  • Arc provides a way to run transformations using system binaries, without the user invoking System.run directly.
  • cast_attachments is used in model changesets to handle the attachment.

Views/Templates

  • Nested models supported using inputs_for. Models handle these fields with cast_assoc.

Javascript & CSS

  • Switched from Brunch to Webpack in 935b7c5.
  • Turbolinks to do page transitions. Fast page loads, and additionally allows player to keep playing between page loads? (3)
  • Semantic UI for admin area CSS framework.
  • Separate assets pipelines for main site and admin area.
  • CSS is SASS
  • Javascript is ECMA 2015 (ES6.0).
  • Uses Umbrella.js for DOM/events/AJAX
  • ES6.0 JS classes are used to describe domain objects and encapsulate their functionality. Episode and Player for example.
  • Admin-only at present.
  • Uses LIKE SQL queries.
  • Multiple or single result type (People, Episodes, etc)
  • Serviced via AJAX.

Transactional Email:

  • Uses Bamboo.
  • SMTP provider is Campaign Monitor.
  • Username and password configured in config as ENV variables.
  • Config picks up environment variables using e.g.: password: {:system, "CM_SMTP_TOKEN”}.
  • auth_view used to generate URLs in emails, passing in the application endpoint instead of a current connection.

Passwordless Logins

  • User visits /in to enter their email and be sent a login email
  • GET /in displays the form
  • POST /in processes the form. Two function definitions, with one being pattern matched to look for email param.
  • Token and expiry set on user in controller action.
  • Error if user not found, user being shown a 404.
  • When in dev environment it gives a login link in the response page, and skips sending the email.
  • Response page checks for @person instance variable, shows confirmation message.
  • 15 minute expiry. Checked after retrieving user, not part of query.
  • Auth token in URL is a Base16 encoded email and random token with a separator, |.
  • GET /in/:token decodes the auth token, checks for user with email and matching random token.
  • One-time-use. Resets the token and expiry on successful login.

Did I get something wrong? Get in touch with me on Twitter.

Followup

Added on 2016-11-16.

  1. I actually mistook this as something that was unused. I created a pull request doing so. Jerod Santo kindly showed me where it was used.
  2. Jerod Santo asked what I meant by this. See my replies there, and also my How I Put Rails Models on a Diet post.
  3. Confirmed. I’m a big fan of Turbolinks.js, even the statically generated Pincount Podcast site uses it.