!Magic

Written by Bryan Powell. For updates, subscribe to the feed, join the newsletter, or follow me on Twitter.

Interaction is an Enhancement

Aaron Gustafson for A List Apart:

In February 2011, shortly after Gawker Media launched a unified redesign of its various properties (Lifehacker, Gizmodo, Jezebel, etc.), users visiting those sites were greeted by a blank stare. Not a single one displayed any content. What happened? JavaScript happened. Or, more accurately, JavaScript didn't happen.

He goes on to explain why, in the web, content should always comes first with interaction as a layer on top. It's interesting that this still has to be pointed out, as this was commonly accepted just a few years ago. As browsers continue to improve, developers talk less about progressive enhancement. But as Aaron points out, browser support isn't the primary issue here.

Reading this article reminds me of a talk I gave last year at All Things Open, where I made the case that we're building at least two kinds of things for the web:

  1. Content-Focused: New York Times
  2. Interaction-Focused: Slack

Each of these warrants a different approach. To readers of the New York Times, content is the priority and nothing should stand between them and their content. What about Slack? Content isn't the priority to those users. Slack is a tool for collaboration, not content delivery. Supporting progressive enhancement feels like overkill.

Going further, why do tools like React and Ember even try to support server-side rendering? At best it's an afterthought that's proven to be brittle. It seems these frameworks are building in backwards-compatibility in an attempt to become the "one tool to rule them all".

Consider this — when a user prioritizes content over interaction, using a client-side JavaScript framework that prioritizes interaction might be a poor choice.

Of Patterns and Power: Web Standards Then & Now

Separating structure from style and behavior was the web standards movement's prime revelation, and each generation of web designers discovers it anew. This separation is what makes our content as backward-compatible as it is forward-compatible (or "future-friendly," if you prefer). It’s the key to re-use. The key to accessibility. The key to the new kinds of CMS systems we're just beginning to dream up. It's what makes our content as accessible to an ancient device as it will be to an unimagined future one.

Zeldman nailed it in this recent piece. The style / behavior separation he's preached his entire career drove my decision to separate structure / logic in Pakyow. Markup, even in a templating system, should be expressed in terms of content. Semantics are just as important during development as they are in production.

Web standards is more relevant than ever.

Hiring in Silicon Valley

Zack Holman on how startups interview candidates:

There's such a wild gulf between what gets asked in interviews and what gets done in the gig's daily grind that it's a wonder how startups make it out of the initial incubation phase in the first place.

Just another reason not to do technical interviews.

F*ck Your 90 Day Exercise Window is also well worth a read.

Is it just me or is there a trend emerging?

Pakyow Intros

We added a new category on the Pakyow Forums for the purpose of introducing yourself to the community. Jump over and tell us a few things about yourself. Intros aside, we've had several good discussions happening lately.

The Self-Destruction of Walled Gardens

We talk a lot these days about the open web and rant about how platforms such as Medium are working against it. To some extent this is all true — the rise of platforms has certainly made the web feel more closed than it did 10 years ago.

However, avoiding these platforms is not the answer.

To most consumers, there’s a real advantage in using a platform like Medium instead of figuring out how to build their own solution. And from their point of view, there's little risk. John O'Nolan talks about this in his recent post:

Mainstream, casual consumers will always be attracted to what is easiest, pretty and cost-efficient. And there is nothing wrong with that.

Sure, you can make predictions and point out how you don’t control your content and Medium can put advertising on it at any time. But until they actually do anything like that, predictions are just hearsay. Nothing has happened yet to break user trust. So why immediately default to distrust?

Closed networks like Medium are prevalent for casual bloggers because open alternatives suck for this use-case. If you just want to write something that looks good and can easily be shared with no professional agenda, you shouldn't have to go through the hassle of deploying an application and an entire server.

I agree with this — particularly the last bit. The fact is Medium has enabled more people to contribute to the web. Platforms solve a real problem. Consumers should use them. But for the sake of the open web, platforms like Medium have a responsibility to become open.

Here's what an open platform looks like:

  • Makes it easy for users to leave with their content
  • Provides tools to make that content accessible again

If someone leaves a platform and can't immediately host a navigable version of their content elsewhere, it's not an open platform. I can think of exactly two reasons a platform wouldn't want to become open:

  1. Technical challenges make it hard
  2. Being open is perceived as being bad for business

The first reason is more of an excuse, really. It's easier than ever to create open platforms and give someone the power to easily host their own content. If being open was a priority, the Medium team could make it happen.

If the first reason is an excuse, the second is complete nonsense. In fact, with a long-term view, being a closed platform is in fact bad for business. Dave Winer makes a compelling case for this in his recent post:

Tech is cyclic. First there was an open platform, then silo-makers were able to build something higher level by foreclosing on the openness. Then they stagnate because big companies get stuck in the Way Things Always Have Been, and the users get skilled, a new generation comes along and they see how to make progress outside the silo and enough people use the new open system so it gains traction. It’s always more exciting than the stale corporate silos, so for a while they blossom, until the cycle repeats.

Closed platforms are doomed to the same fate. Being closed is great at first; it gives a platform ultimate control. But the beautiful walled garden soon becomes an echo chamber, masking the platform's increasing irrelevance until it's too late.

I firmly believe that giving users an easy exit strategy is a great catalyst for long-term success. If 10% of a platform's user-base leaves after launching a controversial feature, that's immediate feedback. Whereas Medium would have slightly fewer articles written or more complaining than usual, an open platform would feel the pain and could adapt. Walled gardens make it too easy to ignore user feedback because there's less pain.

WordPress

Dave Winer recently tweeted that instead of posting to Medium we should post to Tumblr or WordPress.com. I fail to see how those two are more open, but yes let's talk about WordPress.

I have quite a lot of respect for Matt and what the WordPress community has created. It's empowered more contribution and created a more democratic web in the process. Great.

But unlike its open-source counterpart, WordPress.com is not open. As far as I know, there's no easy way to move a blog from WordPress.com onto a standalone server. The best free option they provide is a content export feature, but you're on your own to setup WordPress.

The WordPress platform does offer a "Guided Transfer" service in exchange for a $129 fee. Why can't they automate this process and then make it free? I imagine it's one of the two reasons we discussed earlier, though I'd love to be wrong.


Platforms like Medium and WordPress.com have empowered millions to contribute to the web, but they've dropped the ball on ensuring that these contributions exist well into the future.

This is a missed opportunity.

My team and I are working on an open platform built around our open-source web framework. When someone creates a website on our platform we'll let them export not only their data, but a bootable version of it. Eventually, this feature will also provision a server on a VPS provider (such as DigitalOcean) and move their code to it.

As a community we've spent years fighting vendor lock-in with open-source software. And by now open-source is nearly standard practice. It's about time we fight for open platforms.

Rubocop as a Teaching Tool

Last year I jumped into a project that had started about 9 months earlier. The team had already defined best-practice patterns and decided on preferred syntax. It was also a Node.js project, something I was less familiar with at the time.

Fortunately JSLint was being used as part of the CI build. While frustrating at the beginning, it quickly taught me how the team preferred to write their code. Nobody handed me a style guide. I could write code, run the build, and get immediate feedback.

In about a week I was catching (most) syntax issues without even having to run the build. JSLint effectively taught me the style guide to follow.

When the next Ruby project started, I went looking for a similar tool and found Rubocop. I added it as a build step, just to see the impact it might have. Here are the results:

Rubocop Graph

Each bar represents how many commits out of 10 caused a Rubocop failure.

At first it failed all the time. You can start to see a pretty immediate drop about half way in. This is when we finally fixed the offenses that had already been introduced into the project. There were a lot of these because Rubocop was added after about 2 weeks of development had passed. From that point to present day, there's been a continuous downward trend in the number of offenses per commit.

This points to something interesting. As time has passed, there are fewer offenses introduced into the project that violate the style guide. For myself and the team that I work on, Rubocop has proven to be a pretty good teacher.

Setting Up Rubocop

Installing Rubocop in a Ruby project is easy. Add rubocop to your Gemfile, bundle install and you're ready to roll. If your project has been around for awhile, be prepared for that first run depression to set in. It happens.

To use Rubocop to enforce a style guide, I find it helpful to disable most of the metric cops. Sure, perceived complexity, method length, etc can be important. But in my opinion those are things best dealt with in code reviews rather than being enforced by almighty Rubocop.

In case it's helpful, the full .rubocop.yml from my recent project can be found here.

Lessons Learned

Introducing Rubocop into this project was a positive experience overall. The resulting codebase is both consistent and readable. A word of warning though: get buy-in from the team before you try this. Having a tool pointing things out in your code is annoying. Explain the purpose and ask for feedback.

It worked for us, give it a shot and let me know how it works out!

Replacing Technical Interviews with an Audition

Years ago I asked a candidate for a developer position to solve a variation of the Fizz Buzz problem during an interview. He failed and I moved on, swearing never to do another technical interview again.

I hated the fact that he failed to solve the problem told me nothing about why he failed to solve it. It just made him feel bad without reason, and that made me feel bad too. When the entire point of the technical interview is to better understand the technical abilities of a candidate, this seems like a pretty poor state to leave things in.

Unless your company is named Trivial Programming Puzzle Solver Inc, you won't determine a candidate's abilities during a technical interview (and it doesn't matter if the interview takes place over 30 minutes or over an entire day). As developers, our day-to-day jobs are nothing like a technical interview. The time that we spend working involves a different kind of pressure solving a very different kind of problem. And at good companies, we perform our work without someone else breathing down our necks evaluating our performance.

At Metabahn, we've made the last few hiring decision without performing a single technical interview. And every person hired has worked out well and made big contributions to the company. Here's how we did it.

1. Tell us something interesting

Before we ever talk to a candidate, we ask that they tell us a written story. This is often described as a request in the job posting itself. It doesn't have to be long and it's best if it's non-technical. We want to see what they communicate as well as how good they can do it.

Many candidates are thrown out after this step. We look for things like honesty, personality, and openness -- not a marketing pitch. The fact is, as a developer at Metabahn, you'll spend more time communicating than writing code. As a team, we always err on the side of overcommunicating everything. If you can't communicate, you won't be an effective part of our team.

2. Let's have a real conversation

We actually do perform one or more interviews, they just aren't technical. We'll never ask for a candidate to write some code or solve a problem in front of us. Our interviews tend to be relaxed and enjoyable, because they're just a friendly conversation. We have two main goals with this step:

  1. Make sure our goals as a company are in line with the candidate's personal goals. We want people who work for Metabahn to understand and agree with the vision for the company, while at the same time helping them flourish.

  2. Discover the candidate's specializations, focus, background, and interests. If you've been building embedded systems more than you have websites or web apps, we probably aren't the right company for you.

This interview is just as important for the candidate to learn about us as it is for us to learn about the candidate. If we decide to work together, we're both making huge committments. It has to be a mutual fit.

Need a bigger salary than we can offer? We'll know after this conversation. Have the right background and are interested in building the kinds of websites and apps we build? We'll know that too. Interested in actively impacting the vision and goals of Metabahn? GREAT.

Once we're to this step things are serious. We've narrowed down to one or two candidates for each position and are almost ready to make a decision. How do we pick between them, especially without a technical interview?

3. The technical audition

We take the approach that other companies (such as Automattic) have taken. That is we pay candidates to do real work for us for a small period of time; essentially a technical audition. This gives us real-world metrics to make a decision with. Making the technical audition part of our hiring process is the best decision we've ever made.

To be honest, we stumbled into this approach by accident. It just so happened that early Metabahn employees were contractors first. I was comfortable making a bigger committment to them because I knew the work they could deliver.

So now, we extend an audition to our top candidates. Auditions are a minimum of 10 hours per week for 2 weeks. This is all paid, of course. Candidates don't have to quit their job to audition; nights and weekends are fine.

This gives us a realistic picture of what to expect, perhaps more than other companies because nearly all of our processes are asyncronous. Nearly all team communication happens on Slack and we have one all-hands meeting that takes place on Monday morning (that you don't have to be in person for). Folks are free to work from anywhere whenever they choose. Performance is measured with results, not time spent online or at the office.

Conclusion

This has worked well for us, but it's important to understand this in context of how Metabahn operates as a whole. I fully encourage you to try replacing technical interviews with auditions, but realistically your mileage may vary.

How does your company operate? I'd love to hear from you.

Set Your Agenda

I recently disappeared into the mountains with my family for some long overdue rest. Computers were left at home and devices largely remained off. I didn't check my email for over a week and had my work calls held.

Nothing bad happened. The Metabahn team handled things perfectly. Clients remained happy, important work got done, and I came back to several promising new leads. It's nice to not be a required component all the time.

Stepping away for a few days led to the realization that few things in any given week can be classified as an emergency. Maybe this sounds obvious, but in context of the burnout I felt prior to getting away, it was profound.

The weeks before I left were filled with one emergency after another. I jumped around from task to task, accomplishing little overall, but happy to be averting disaster after disaster. Fortunately these emergencies and so-called disasters were merely exaggerations of reality.

Taking a step back, what is an emergency? The way I defined it, an emergency is a situation that is in contradiction with one's overall goals and vision.

Here's a summary of my goals:

  1. Maintaining my personal health and well-being (physical and mental).
  2. Nurturing healthy relationships with family and friends.
  3. Keeping the company operating smoothly to benefit our clients and ourselves.
  4. Improving our open-source work to empower anyone wanting to build for the web.

Having this list brings a lot of clarity to otherwise difficult situations. For example, if something in the company isn't running smoothly but the first two goals aren't being met, my best option is to focus on the first two goals and delegate the third. That's exactly why I stepped away in the middle of things. And the entire company is better for it.

Set your agenda and stick to it.

The flat company myth.

Flat

I've been wanting to write about my experience starting and running Metabahn for some time now. Unfortunately, running a software company is very much at odds with absolutely everything else I could do with my time. So, let me catch you up.

Backstory

September of 2007 was a busy month. I left the startup I was working at, started Metabahn, and got engaged; in that order. At this point the company was more a cover for my own consulting work; I wasn't all that interested in growing it.

In March of 2008 I found a project to build a web-based tool that would be resold to major financial institutions. I put a lot of work into landing the project, as I knew it would pay the bills for the next few months and give me at least the illusion of stability. When it came through I knew it was too big to do by myself.

I hired my first contractor and we delivered the project on time. This happened to be the first of many sizable projects that dwarfed the amount of effort I could expend. So I kept hiring contractors to help. Overall I have positive memories of this time. It was the single biggest learning experience I've ever had, filled with success and failure alike.

Version 2.0

2012 rolled around and I was beginning to sense change in the air. If you know me reasonably well you know I don't like to stagnate. The company as it was just didn't seem to be challenging enough. Couple that with the momentum we had built and it felt like the right time to try and build a real company, with real employees.

GASP.

I reached out to a long-time friend about joining as the first employee. He had contracted with us a couple years prior, and I knew he'd be a good fit for the direction I wanted to take the company. We'd talked about working together again, and it seemed like the right time.

Metabahn 2.0 started in May of 2012 when I made the first hire. Today we're at 2.3 with six employees and roughly the same number of contractors who help us out on a consistent basis. We've been bootstrapped from day one and have taken no outside money. 100% of the company is owned either by myself or other employees.

Flatland

As we hired more folks, I did a lot of thinking about process. My view is if you can't define the problem to solve with a process, don't implement the process. Conversely, if the problem goes away, throw away the process that was being used to solve it. Without these rules you risk becoming bloated, even at a small size.

This led to a really flexible work environment, including flexible hours and unlimited vacation time. To this day, we really only have a single rule:

Don't make decisions that have a negative impact on the team.

Along with this flexibility came the lack of formal organizational structure. It is a fad in the tech industry to have a so-called "flat" company. However, all companies have an implied hierarchy. This is evident when there's a decision to be made.

Let's say some member of the team makes a product decision that causes a negative experience for a large percentage of customers. Someone has to deal with the problem, right? As it turns out, someone always does deal with it.

The biggest problem with flat companies is that when structure is left undefined, a structure will still form, but it won't be the structure you want. Folks will elect themselves to particular positions and start making decisions on behalf of others. So, you still don't avoid the hierarchy and you've also created an unhealthy organization.

When we started growing, I decided to implement structure amounting to a flat hierarchy.

WAT.

Here's how it works. Write down all the roles that exist in your company. Assign each role to a member of the company and inform them of their responsibility. Then tell them not to play that role unless they need to.

This levels the playing field for day to day operations, effectively creating a flat company. But when a situation arises and a decision needs to be made, it's clear who decision maker is. Just like process, the structure isn't used unless there's a defined problem to solve.

I tend to think of it a bit like sudo in a Unix system.

You can find this pattern in everything we do as a company. Typical project teams consist of three developers; one is chosen to play the role of project owner. Our teams don't require full-time management, so the owner is a developer 80% of the time. When the client calls about a problem, the owner deals with it rather than letting it impact the entire team.

I even think of my job as Sudo CEO. By default, I consider myself to be a developer. I just also have the power to make higher-level decisions when necessary. At our size this works really well. I hope it scales.

Pakyow Platform

A few days ago the Metabahn team rolled out a teaser page for Pakyow Platform, a new project we've been hard at work on the last few months. I couldn't be happier with my team's work on this project and we can't wait to introduce you to it in the near future!

We'll be putting more details on the site over the next few weeks as we get closer to a public rollout. In the meantime, you should join the launch list →

Your template language is killing the web.

I published an article about template languages and how they effect the modern web:

It used to be simple to build things for the web. In some ways this is still the case, but modern realtime apps are driving the complexity factor to new and uncomfortable heights.

You need knowledge of more tools and technologies than ever to build a web app that feels modern. In contemplating this shift, much of the added complexity stems from how we handle the part of the application a user interacts with — the view layer.

This is likely the start of many such articles in which I try to explain some of the more nuanced thinking behind what we're doing with Pakyow and the impending realtime UI libraries.

Read it →

Unedited Episode 3

Posting this a bit late, but this was a fantastic episode from RailsConf 2015 in which we discuss ActionCable (new realtime layer for Rails), Pakyow, and the future of the web.

Listen →

Unedited Episode 2

The episode in which Wayne and I discuss keeping software development teams on the same page, why no management is bad for your company, and communism.

Listen →

Handling user creation & auth with Pakyow.

User creation and auth is a simple concept but can be tricky to implement because it consists of so many moving parts. Today I want to try and make it a bit more approachable by outlining aspects of the approach we use at Metabahn.

As a precursor to this article, please read Connecting a Pakyow app to Postgres with Sequel. You'll need this knowledge once we start implementing the backend code. I'd even recommend that uou could even use that project as a starting point for this project.

For reference, you can find the complete app here.

Building the prototype.

Let's start by building the frontend. This will help us get a feel for what we're building before we start writing any backend code. Sort of a roadmap, if you will. We'll begin with the signup form.

The signup form.

Create a new view named app/views/users/new.html. In it, create a form that looks something like this:

<h2>
  Please Signup
</h2>

<form data-scope="user" data-prop="action">
  <input type="submit" value="Sign me up!">
</form>

This creates a form scoped as user. Later, our backend code will be able to bind user data to this form as well as receive user data from the form submission. It will also handle setting the action and method of the form for us.

We need three fields to create a user: email, password, and password confirmation. Let's add those next:

<h2>
  Please Signup
</h2>

<form data-scope="user" data-prop="action">
  <label>Email</label>
  <input type="text" data-prop="email">

  <label>Password</label>
  <input type="password" data-prop="password">

  <label>Confirm Password</label>
  <input type="password" data-prop="password_confirmation">

  <input type="submit" value="Sign me up!">
</form>

Notice that these are labeled as props that coorespond to the fields we know our user will have. When the backend is hooked up, Pakyow will handle setting the field names for these fields.

We also need a way of handling validation errors. Let's do that by adding an errors scope:

<h2>
  Please Signup
</h2>

<form data-scope="user" data-prop="action">
  <ul data-scope="errors">
    <li data-prop="message">
      Error message goes here.
    </li>
  </ul>

  <label>Email</label>
  <input type="text" data-prop="email">

  <label>Password</label>
  <input type="password" data-prop="password">

  <label>Confirm Password</label>
  <input type="password" data-prop="password_confirmation">

  <input type="submit" value="Sign me up!">
</form>

Now we have a user creation form that's ready to be tied inot the backend. Let's do the same with the login form.

The login form.

We'll take a similar approach as we did with the signup form. First, let's create a form; this time it will be scoped as session. This should be created as app/views/sessions/new.html:

<h2>
  Please Log In
</h2>

<form data-scope="session" data-prop="action">
  <input type="submit" value="Log in!">
</form>

Now we need fields for email and password:

<h2>
  Please Log In
</h2>

<form data-scope="session" data-prop="action">
  <label>Email</label>
  <input type="text" data-prop="email">

  <label>Password</label>
  <input type="password" data-prop="password">

  <input type="submit" value="Log in!">
</form>

As well as a way to handle errors:

<h2>
  Please Log In
</h2>

<form data-scope="session" data-prop="action">
  <ul data-scope="errors">
    <li data-prop="message">
      Error message goes here.
    </li>
  </ul>

  <label>Email</label>
  <input type="text" data-prop="email">

  <label>Password</label>
  <input type="password" data-prop="password">

  <input type="submit" value="Log in!">
</form>

And that's it. Start the server with pakyow s, open up the user form and session form in your browser, and bask in the glory of view composition without backend code!

Make a mental note that anyone with basic frontend skills can create these views. If you work on a team with dedicated designers and developers, it's handy to have your own areas to work in without stepping on each other's toes. Even if not, it's still helpful to think of the frontend and backend as completely separate steps. For me, anyway :-)

Onward to the backend!

Setting up Rack session middleware.

Before we get into building the backend, we need to tell Pakyow to use the Rack session middleware. Open app.rb and add this bit of code within the app definition:

middleware do |builder|
  builder.use Rack::Session::Cookie, key: 'myapp.session', secret: 'secretgoeshere'
end

You'll likely want to change the key and secret.

Installing bcrypt.

Next we need to add the bcrypt library to our app. This will let us encrypt the password in the database so it can't be stolen. Add the following to your Gemfile:

gem "bcrypt"

Now run bundle install to install the library.

Setting up the user migration.

We'll want to store users in the database, which means we need to create a migration that defines the schema for our users table. Create a file in the migrations directory named 001_create_users.rb (the leading number might be different if you already have migrations in your app). Add the following:

Sequel.migration do
  up do
    create_table :users do
      primary_key   :id
      String        :email
      String        :crypted_password
      Time          :created_at
      Time          :updated_at
    end
  end

  down do
    drop_table :users
  end
end

Notice the crypted_password field; this allows us to store a password securely. Even if the data was to leak, the passwords would be safe.

Run rake db:migrate to create the table in your configured database.

Setting up the User & Session models.

We need two models to handle user creation and auth. Let's start with the one for User.

The User model.

The User model will be built on Sequel so that users can be persisted to the database. Start by creating a file at app/lib/models/user_model.rb (note that the naming convention can be anything since all the files within lib are loaded for us).

Add the following bit of code to define our model:

require 'bcrypt'

class User < Sequel::Model
end

As we discussed when we defined the migrations, we'll only be storing the crypted password. This means that there is no password or password confirmation fields available on our User model. Let's define some attributes so that we can set these values from the submitted form:

attr_accessor :password, :password_confirmation

Now when we do User.new({ password: 'foo' }) the value for password will be available on the model (same goes for password_confirmation).

Let's handle encrypting our password by overriding the password setter:

def password=(password)
  @password = password

  return if password.nil? || password.empty?
  self.crypted_password = BCrypt::Password.create(password)
end

We should also normalize the value for email so that it is case-insensitive. One approach is to add a before_validation hook that's executed right before the model is validated and saved:

def before_validation
  @email = @email.to_s.downcase
  super
end

Now let's create our validations. Add the following code into the User class:

plugin :validation_helpers
EMAIL_REGEX = /^[A-Z0-9._%+-]+@(?:[A-Z0-9-]+\.)+[A-Z]{2,4}$/i unless defined? EMAIL_REGEX

def validate
  super
end

The first validation we want to write is for email. Add the following code into the validate method:

# require a value for email address
validates_presence  :email

# require a valid email address
validates_format    EMAIL_REGEX, :email if email && !email.empty?

# make sure the email address is unique
validates_unique    :email

Now let's define validations for password (again, to the validate method):

# require a value for password
validates_presence  :password

# make sure the password matches the confirmation
errors.add(:password, "and confirmation must match") if password && password != password_confirmation

Finally, we need a method that performs the authentication check on login attempts. The way this will work is when a login attempt comes through, the sessions#create route will create a Session object with the submitted email and password. The route will then ask the User model if the session is valid.

We'll need two methods on the User model to make this work. First, a class-level method called auth:

def self.auth(session)
  user = first(email: session[:email])

  if user && user.auth?(session.password)
    return user
  else
    return nil
  end
end

This method accepts a session object, finds the first user with a matching email address, and calls an auth?method on the instance of the found user. Let's define the auth? method now:

def auth?(password)
  BCrypt::Password.new(crypted_password) == password
end

The auth? method will return a truthy or falsey value depending on if the provided password matches the crypted_password stored on the user instance.

The Session model.

Fortunately the Session model is much simpler. Create a session_model.rb file within app/lib/models with the following code:

class Session
  attr_accessor :email, :password

  def initialize(params)
    @email, @password = params.values_at(:email, :password)
  end

  def [](key)
    send(key) if respond_to?(key)
  end
end

Here we simply provide a bit of code for initializing the session, along with a convenience method for fetching values with a Hash-style syntax (e.g. session[:email]).

Writing the routes.

Whew, okay we now have views and models. Our dependencies are also setup and ready for use. Let's tie it all together with a few routes. The first step is to define Restful resources for user and session. Open up app/lib/routes.rb and add the following code:

restful :user, '/users' do
  new do
  end

  create do
  end
end

restful :session, '/sessions' do
  new do
  end

  create do
  end

  remove do
  end
end

For Pakyow to setup our user and session forms for us, we'll also need to define restful bindings. Open app/lib/bindings.rb and add the following code:

scope :user do
  restful :user
end

scope :session do
  restful :session
end

Finally, we'll need a few helpers for handling errors within our forms. Add the following code to app/lib/helpers.rb:

def handle_errors(view)
  if @errors
    render_errors(view, @errors)
  else
    view.scope(:errors).remove
  end
end

def render_errors(view, errors)
  unless errors.is_a?(Array)
    errors = pretty_errors(errors.full_messages)
  end

  view.scope(:errors).with do
    prop(:message).repeat(errors) { |context, message|
      context.text = message
    }
  end
end

def pretty_errors(errors)
  Array(errors).map { |error|
    error.gsub('_', ' ').capitalize
  }
end

We won't walk through each line of this today, but I'll likely write a post in the future just on error handling.

Alright, now let's dive into each individual route.

users#new

In this route we want to setup the form and present any errors associated with a user being created. Add the following code to the new action of restful :user:

view.scope(:user).with do |view|
  view.bind(@user || User.new)
  handle_errors(view)
end

This code finds the user scope (which is the form in our case) and creates a working context with this scope using the with method. Within that context we do two things:

  1. Bind a user object (or a brand new object if one doesn't exist).
  2. Call the handle_errors helper method to present any errors.

It might seem a little odd that we're handling existing user instances and errors in this action, but it'll make sense after we define the users#create action.

users#create

Here we want to create the user object, so long as it's valid. If it isn't valid we want to take the user back to the form and present the errors. This is what the code within the create action should look like:

@user = User.new(params[:user])

if @user.valid?
  @user.save
  redirect router.path(:default)
else
  @errors = @user.errors
  reroute router.group(:user).path(:new), :get
end

Here we create a User instance from the submitted form values and check the validity. If it's valid, we save the user and redirect back to the default route. Otherwise we set the errors in an instance variable and reroute the user back to users#new. Rerouting is different than redirecting, as it happens within the current request/response lifecycle. It's simply a way of executing the logic tied to some other route.

Now it's time to hookup the session routes.

session routes

The session routes are so similar in structure to the user routes that I'll provide all of the necessary code at once:

restful :session, '/sessions' do
  new do
    view.scope(:session).with do |view|
      view.bind(@session || Session.new({}))
      handle_errors(view)
    end
  end

  create do
    @session = Session.new(params[:session])
    if user = User.auth(@session)
      session[:user] = user.id
      redirect router.path(:default)
    else
      @errors = ['Invalid email and/or password']
      reroute router.group(:session).path(:new), :get
    end
  end

  remove do
    session[:user] = nil
    redirect router.path(:default)
  end
end

A couple notes here. On sessions#create, If the authentication attempt is successful we'll create a user session. This allows us to keep track of the currently logged in user. Notice also in the remove action how we set the user session to nil.

Convenience Routes

I find it helpful to define convenience routes for login / logout. Add the following to your routes file:

get :login, '/login' do
  reroute router.group(:session).path(:new)
end

get :logout, '/logout' do
  reroute router.group(:session).path(:remove), :delete
end

Now your users can login and logout by going directly to /login or /logout.

Restricting Routes

Now that we have a fully baked user creation / auth system, let's put a route behind a privacy wall. We'll do this by defining a route function that can be mixed into other routes as a before hook. Define the following function in your routes file:

fn :require_auth do
  redirect(router.group(:session).path(:new)) unless session[:user]
end

Assuming you've used the pakyow-example-sequel repo as a starting point, define a before function on the default route like this:

default before: [:require_auth] do
  view.scope(:post).apply(Post.all)
end

Now when you navigate to / you'll be redirected to the login page unless you're an authenticated user. Handy!

Playing with our app.

It's now possible to do all kinds of things with our app. Try some things:

Conclusion

And there you have it! Hit problems or have questions? Post on Stack Overflow or ask us for help on Gitter. Thanks for reading!

Connecting a Pakyow app to Postgres with Sequel.

Pakyow doesn't ship with an ORM (object-relational mapper) layer. This reflects a design decision we made early on to be unopinionated about such things. Instead, we let the user choose the ORM that they're most comfortable with. There are some good options out there, including Sequel, ROM, and ActiveRecord.

The ORM we use most often at Metabahn is Sequel. It's stable, performant, and stays out of our way. Today I'd like to show how we've integrated Sequel into the dozens of Pakyow apps we've shipped to production over the last couple of years. Postgres is our database of choice in most cases, so we'll be talking specifically about that here.

For reference, you can find the complete app here.

Setting up dependencies

Make sure you have Postgres installed and running locally. If you're on a Mac, Postgres.app is the easiest way to get going. Once Postgres is running, open up your Gemfile and add three new gems:

group :development do
  gem "dotenv"
end

gem "sequel"
gem "pg"

Note that we only want to use dotenv in development as there are better configuration strategies for production environments.

Run bundle install to make sure your dependencies are up to date. Next, open app.rb and require sequel (after require 'bundler/setup'):

require 'sequel'

It's also helpful to have Sequel automatically add the created at and updated at timestamps to our data. To enable this behavior, add the following code underneath the call to require:

Sequel::Model.plugin :timestamps, update_on_create: true

Inside the development configuration block add the following code to load your environment:

configure :development do
  require 'dotenv'
  Dotenv.load
end

You're good to go!

Environment-specific configuration

We'll use the Dotenv library to keep configuration details out of app.rb. Create a .env file in the root application directory (it's also a good idea to ignore in your .gitignore file). Define a variable that contains the database connection string:

DATABASE_URL=postgres://{user}@localhost/{database}

Replace {user} and {database} with your own username and database you'd like to use.

Creating the database connection

Back in app.rb add the following bit of code to the development configuration block to create a database connection:

$db = Sequel.connect(ENV['DATABASE_URL'])

This tells Sequel to connect to the database configured for your environment. It stores this connection in a global variable for use throughout the application. Note that you'll need to setup the connection in your production environment as well based on your production configuration strategy. If you're deploying to Heroku you can use the exact same code.

Setting up models + migrations

Now that we have a connection, let's create a model. For our purposes, let's create a model that represents a blog post. Create a new app/lib/models directory with a new file named post_model.rb. Add the following code:

class Post < Sequel::Model; end

Next, we need to create a migration that defines the schema for our new model. Create a migrations directory in the root application directory with a new file named 001_create_posts.rb. Add the following code:

Sequel.migration do
  up do
    create_table :posts do
      primary_key :id
      String :title
      Text :body
      DateTime :created_at
      DateTime :updated_at
    end
  end

  down do
    drop_table :posts
  end
end

Now Sequel will automatically create our tables when running the migrations. Note that the migration file names are important. You can read more about migrations the naming strategy itself in the Sequel docs.

Database maintenance tools

We perform database maintenance via Rake tasks. I've published an example rakefile that contains common tasks for creating, deleting, and migrating your database. Copy these tasks into your Rakefile and run rake db:setup to create and migrate your database.

Presenting data in a Pakyow view

Before we can present data we should create some for testing. To keep it easy for this tutorial, we'll use console. Run pakyow console and enter the following commands:

irb(main):001:0> Post.create(title: 'One', body: 'This is the first post!')
=> #<Post @values={:id=>1, :title=>"One", :body=>"This is the first post!", :created_at=>2015-04-10 16:12:40 -0500, :updated_at=>2015-04-10 16:12:40 -0500}>
irb(main):002:0> Post.create(title: 'Two', body: 'This is the second post!')
=> #<Post @values={:id=>2, :title=>"Two", :body=>"This is the second post!", :created_at=>2015-04-10 16:12:49 -0500, :updated_at=>2015-04-10 16:12:49 -0500}>
irb(main):003:0> Post.create(title: 'Three', body: 'This is the third post!')
=> #<Post @values={:id=>3, :title=>"Three", :body=>"This is the third post!", :created_at=>2015-04-10 16:12:55 -0500, :updated_at=>2015-04-10 16:12:55 -0500}>

Now we have three posts in our database. Type exit and hit enter to exit console.

Let's create a view that we'll use to present our posts. Create an index.html file in app/views. Add the following HTML:

<div data-scope="post">
  <h1 data-prop="title">
    This is the post title
  </h1>

  <p data-prop="body">
    Post body goes here.
  </p>
</div>

Run pakyow server to start the server, then navigate to localhost:3000 to see the new view. Now let's bind our post data to it. Open app/lib/routes.rb and define a default route. Here's what it should look like:

Pakyow::App.routes do
  default do
    view.scope(:post).apply(Post.all)
  end
end

Reload your browser and you'll see three three posts we created earlier.

Conclusion

That's all there is to it! I hope this gives you some valuable knowledge in building your next Pakyow app. Hit problems or have questions? Post on Stack Overflow or ask us for help on Gitter. Thanks for reading!

Metabahn Unedited

The first episode of Unedited, the Metabahn Podcast, was published today. This is something we've been wanting to do for a very long time. When it came down to it, we never felt like we had the time to do it properly.

Last week Wayne Schroer and I had the idea of doing a completely unedited podcast (hence the name). The format would be laid back and Metabahn folks + friends would simply discuss one topic that's been on our minds for the week. We would mix the audio live and record into a solid state recorder. One take, no post-processing, and it's done.

I think the first episode went pretty well and proved that this format can work. The best part? It took us less than an hour to record, upload the audio, and write the show notes.

Looking forward to next week!