CouchDB on Wheels

Ely Service now runs on CouchDB. Things just got a little simpler: no more Django plus PostgreSQL plus Nginx.

Casual Lofa: World's fastest furniture
Casual Lofa: the World's fastest furniture

Ely Service is, as J. Chris Anderson put it, “just a very ordinary-looking garage Web site”. It's a simple Web site, which I originally developed using Django. It consists of six pages, one of which has a contact form for sending emails. So the requirements are very straightforward.

Why switch?

This was an experiment to see how easy it is to develop a simple Web site using CouchDB and (almost) nothing else. Ely Service is essentially a static Web site, and hence barely exploits any of the roaring power of CouchDB's B-Tree index or its distributed capabilities.

CouchApp

CouchApp is a set of scripts that make developing standalone CouchDB applications a lot simpler. Using Futon to do this at the moment is far too painful, although I could imagine a lightweight IDE that allows various show/list functions to be previewed as they are developed. Patches welcome!

In a nutshell, CouchApp allows you to store your map/reduce views, lists, shows and validation functions as files in a directory tree. You can also include various helper functions and templates, which are inserted using macros before being pushed to the database.

I put the majority of the Ely Service site into its own app and the contact form handler into a separate app. Complex sites may consist of many apps that work together. Here is the structure of the "elyservice" application:

elyservice/
  _attachments/
  lib/
    helpers/
      ejs.js
  templates/
    layout/
      head.html
      tail.html
  shows/
    contact.js
    page.js

Show Me

As this is a simple site with only 6 static pages, these are all generated using simple "show" functions.

function (doc, req) {
  // !json templates
  // !code lib/helpers/couchapp.js
  // !code lib/helpers/ejs.js
  var body = new EJS({
    text: templates.layout.head + templates.page + templates.layout.tail
  }).render({
    assets: assetPath(),
    doc: doc
  });
  return {
    headers: {'Content-Type': 'text/html; charset=UTF-8'},
    body: body
  };
}

Using CouchDB to send E-mail

This is the most complex part of the site, as it requires the use of an external process to send the emails. Strictly speaking, an external process is not necessary; a cron job would also do the job just fine.

I decided to write this as a generic CouchApp so it could be reused across multiple sites. Pretty much every site has a contact form of some kind.

This works like a standard UNIX mail spooler. New messages are created with a status of "spool", and the notification script sets the status to "sent" when it has finished sending. Unsent messages are retrieved by the notification script by calling the "mail_spool" view:

function (doc) {
  if (doc.type == 'mail' && doc.status == 'spool')
    emit(null, null);
}

The actual sending of email is done by the send_emails.py script, which is launched as an external process.

I've put the contact form code here, including the mail spooler: http://github.com/jasondavies/couchdb-contact-form/tree/master

Nginx Configuration

One of the only remaining hurdles to truly pure CouchApps is support for clean URLs. I wanted to retain the clean URLs of the original Ely Service site, and in order to do this I had to rewrite them using a reverse proxy. Nginx was ideal for this task.

server {
    listen 89.145.97.172:80;
    server_name www.elyservice.co.uk;
    set $projectname elyservice;

    location / {
        if ($request_method !~ ^(GET|HEAD)$) {
            return 444;
        }

        proxy_pass http://127.0.0.1:5984/elyservice;
        proxy_redirect default;
        proxy_set_header X-Orig-Host '$host:$server_port';

        rewrite ^/media/(.+)$ /$projectname/_design/elyservice/$1 break;
        rewrite ^/$ '/$projectname/_design/elyservice/_show/pages' break;
        rewrite ^/(.*)/$ '/$projectname/_design/elyservice/_show/pages/pages:$1' break;

        return 404;
    }

    location /contact/ {
        if ($request_method !~ ^(GET|HEAD|POST)$) {
            return 444;
        }

        proxy_pass http://127.0.0.1:5984/elyservice;
        proxy_redirect default;
        proxy_set_header X-Orig-Host '$host:$server_port';

        if ($request_method = POST) {
            rewrite ^/contact/$ /$projectname/ break;
        }
        rewrite ^/contact/$ '/$projectname/_design/elyservice/_show/contact' break;

        return 404;
    }
}

It turns out that Nginx automatically decodes URL-encoded characters in rewrite URLs before passing them through the proxy. Hence I couldn't use "pages/foo" for my docids. No problem here, I simply elected to use "pages:foo" instead.

It's worth noting that support for a CouchDB rewrite handler is under active discussion at the moment, so watch this space.

Security and Validation

Validation is very important; I don't want d00dz being able to edit any document in the database. All it takes is a POST or a PUT and anyone can create or update any document. To prevent this, first of all I added an admin user to local.ini. This user is given the special role of "_admin", which has the special priviledge of being able to create and modify design docs.

However, this level of security is not enough, as someone could still PUT malicious text to the home page doc for example.

A simple way to prevent this is to configure Nginx to reject any requests that aren't HEAD or GET:

if ($request_method !~ ^(GET|HEAD)$) {
    return 444;
}

Note: the non-standard error code 444 causes nginx to drop the connection (see https://calomel.org/nginx.html). The standard "forbidden" error code 403 could be used instead.

There may be cases, though, where we want users to be able to create/modify some documents but not others. For Ely Service, we want anonymous users to be able to create new documents of type "mail", but nothing else. This is where validate_doc_update comes in handy.

function (newDoc, oldDoc, userCtx) {
  // !code _attachments/validate.js
  if (userCtx.roles.indexOf('_admin') != -1) {
    return;
  }
  if (oldDoc == null) {
    return validate(newDoc);
  }
  throw {
    forbidden: "Invalid operation: existing messages cannot be modified."
  };
}

Conclusion

Although CouchDB is still alpha software, developing and deploying a simple Web site using CouchApp was very straightforward. The real benefits of CouchDB were not exploited at all, but we'll see some of that in a future post.

Several people have noted that Ely Service loads very quickly. This is a combination of CouchDB's raw speed and the simplicity of Ely Service's design.