diff --git a/.gitignore b/.gitignore
index 91ddf1f..6ec616c 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,22 +1,23 @@
 # See https://help.github.com/articles/ignoring-files for more about ignoring files.
 #
 # If you find yourself ignoring temporary files generated by your text editor
 # or operating system, you probably want to add a global ignore instead:
 #   git config --global core.excludesfile '~/.gitignore_global'
 
 # Ignore bundler config.
 /.bundle
 
 # Ignore the default SQLite database.
 /db/*.sqlite3
 /db/*.sqlite3-journal
 /db/*.local
 /db/database.yml
 
 # Ignore all logfiles and tempfiles.
 /log/*.log
 /tmp
+/doc
 *.swp
 config/database.yml
 config/beanstalk.yml
 .ruby-version
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..7e3f459
--- /dev/null
+++ b/README.md
@@ -0,0 +1,74 @@
+# WebDNS
+
+`webdns` is a web PowerDNS frontend using the powerful pdns MySQL backend.
+
+## Features
+
+ * Automatically handle zone serial updates using multiple serial strategies.
+ * DNSSEC support.
+ * Slave & Master support.
+ * Smart editing helpers.
+ * A flexible per group permission model.
+
+## Installation
+
+`webdns` was developed to be deployed using standard debian jessie (stable) packages and practices. It does not require bundler for production, but is should be pretty straightforward to set it up using bundler on other platforms.
+
+### Setup powerdns
+```
+$ sudo apt-get install pdns-server pdns-backend-mysql
+$ cat /etc/powerdns/pdns.d/pdns.local.gmysql.conf
+# MySQL Configuration
+launch+=gmysql
+
+# gmysql parameters
+gmysql-host=127.0.0.1
+gmysql-port=3306
+gmysql-dbname=webdns
+gmysql-user=webdns
+gmysql-password=password
+gmysql-dnssec=no
+```
+
+You might also want to enable slave and master support on powerdns in order to manage slave & master zones in WebDNS.
+
+### Prepare for deploy
+
+Install dependencies
+
+```
+$ cat Gemfile | grep -oE 'pkg:[a-z0-9-]+' | cut -d: -f2 | xargs echo apt-get install
+```
+
+Edit & install
+
+```
+contrib/systemd/sudo_unicorn -> /etc/sudoers.d/unicorn
+contrib/systemd/default_unicorn -> /etc/default/unicorn
+contrib/systemd/unicornctl -> /usr/local/bin/unicornctl
+contrib/systemd/unicorn.service -> /etc/systemd/system/unicorn.service
+```
+
+```
+systemctl daemon-reload # Notify systemd about the newly installed unicorn service
+```
+
+You should also create an empty database and an account.
+
+### Setup Capistrano
+
+On the server create the following files under `/srv/webdns/shared/config/`. Those files will be symlinked to `config/` on deploy by capistrano.
+
+```
+database.yml
+secrets.yml
+local_settings.rb # Override config/initializers/00_settings.rb
+
+```
+Locally create `config/deploy/production.rb` based on the sample file.
+
+```
+$ apt get install capistrano
+$ cap production deploy
+
+```
diff --git a/README.rdoc b/README.rdoc
deleted file mode 100644
index dd4e97e..0000000
--- a/README.rdoc
+++ /dev/null
@@ -1,28 +0,0 @@
-== README
-
-This README would normally document whatever steps are necessary to get the
-application up and running.
-
-Things you may want to cover:
-
-* Ruby version
-
-* System dependencies
-
-* Configuration
-
-* Database creation
-
-* Database initialization
-
-* How to run the test suite
-
-* Services (job queues, cache servers, search engines, etc.)
-
-* Deployment instructions
-
-* ...
-
-
-Please feel free to use a different markup language if you do not plan to run
-<tt>rake doc:app</tt>.
diff --git a/app/helpers/flash_helper.rb b/app/helpers/flash_helper.rb
index a1b2085..5a50542 100644
--- a/app/helpers/flash_helper.rb
+++ b/app/helpers/flash_helper.rb
@@ -1,26 +1,27 @@
 module FlashHelper
 
+  # Generate html flash messages based on bootstrap alerts.
   def flash_messages
     flash.each do |msg_type, message|
       concat(
         content_tag(:div, message, class: "alert #{bootstrap_class_for(msg_type)} fade in") do
           concat content_tag(:button, 'x', class: 'close', data: { dismiss: 'alert' })
           concat message
         end
       )
     end
 
     nil
   end
 
   private
 
   def bootstrap_class_for(flash_type)
     {
       success: 'alert-success',
       error:   'alert-danger',
       alert:   'alert-warning',
       notice:  'alert-info'
     }.fetch(flash_type.to_sym, flash_type.to_s)
   end
 end
diff --git a/app/helpers/records_helper.rb b/app/helpers/records_helper.rb
index 98283f0..9ea218e 100644
--- a/app/helpers/records_helper.rb
+++ b/app/helpers/records_helper.rb
@@ -1,8 +1,14 @@
 module RecordsHelper
 
+  # Smart suffix for records
+  #
+  # On forward zones returns the zone name.
+  # On reverse zones returns the zone name but also tries to infer the subnet.
+  #
+  # Returns a smart suffix string.
   def name_field_append(record)
     return ".#{record.domain.name}" if not record.domain.reverse?
 
     ".#{record.domain.name} (#{record.domain.subnet})"
   end
 end
diff --git a/app/models/domain.rb b/app/models/domain.rb
index 6ab3f0c..2e2a7fc 100644
--- a/app/models/domain.rb
+++ b/app/models/domain.rb
@@ -1,92 +1,99 @@
 class Domain < ActiveRecord::Base
   self.inheritance_column = :nx
 
+  # List all supported domain types.
   def self.domain_types
     [
       'NATIVE',
       'MASTER',
       'SLAVE',
     ]
   end
 
   belongs_to :group
   has_many :records
   has_one :soa, class_name: SOA
 
   validates :group_id, presence: true
   validates :name, uniqueness: true, presence: true
   validates :type, presence: true, inclusion: { in: domain_types }
   validates :master, presence: true, ipv4: true, if: :slave?
 
   after_create :generate_soa
   after_create :generate_ns
 
   attr_writer :serial_strategy
+
+  # Get the zone's serial strategy.
+  #
+  # Returns one of the supported serial strategies.
   def serial_strategy
     @serial_strategy ||= WebDNS.settings[:serial_strategy]
   end
 
+  # Returns true if this a reverse zone.
   def reverse?
     name.end_with?('.in-addr.arpa') || name.end_with?('.ip6.arpa')
   end
 
+  # Returns true if this is a slave zone.
   def slave?
     type == 'SLAVE'
   end
 
   # Compute subnet for reverse records
   def subnet
     return if not reverse?
 
     if name.end_with?('.in-addr.arpa')
       subnet_v4
     elsif name.end_with?('.ip6.arpa')
       subnet_v6
     end
   end
 
   private
 
   def subnet_v4
     # get ip octets (remove .in-addr.arpa)
     octets = name.split('.')[0...-2].reverse
     return if octets.any? { |_| false }
 
     mask = 8 * octets.size
     octets += [0, 0, 0, 0]
 
     ip = IPAddr.new octets[0, 4].join('.')
 
     [ip, mask].join('/')
   end
 
   def subnet_v6
     nibbles = name.split('.')[0...-2].reverse
     return if nibbles.any? { |_| false }
 
     mask = 4 * nibbles.size
     nibbles += [0] * 32
 
     ip = IPAddr.new nibbles[0, 32].in_groups_of(4).map(&:join).join(':')
 
     [ip, mask].join('/')
   end
 
   # Hooks
 
   def generate_soa
     soa_record = SOA.new(domain: self)
 
     soa_record.save!
   end
 
   def generate_ns
     return if slave?
     return if WebDNS.settings[:default_ns].empty?
 
     WebDNS.settings[:default_ns].each { |ns|
       Record.find_or_create_by!(domain: self, type: 'NS', name: '', content: ns)
     }
   end
 
 end
diff --git a/app/models/record.rb b/app/models/record.rb
index 3e3f1a6..26f9b1b 100644
--- a/app/models/record.rb
+++ b/app/models/record.rb
@@ -1,161 +1,198 @@
 require 'ipaddr'
 require_dependency 'drop_privileges_validator'
 
 class Record < ActiveRecord::Base
   belongs_to :domain
   # Powerdns inserts empty records on slave zones,
   # we want to hide them
   #
   # http://mailman.powerdns.com/pipermail/pdns-users/2013-December/010389.html
   default_scope { where.not(type: nil) }
 
+  # List all supported DNS RR types.
   def self.record_types
     [
       'A', 'AAAA', 'CNAME',
       'MX',
       'TXT', 'SPF', 'SRV', 'SSHFP',
       'SOA', 'NS',
       'PTR', 'NAPTR'
     ]
   end
 
+  # List types usually used in forward zones.
   def self.forward_records
     record_types - ['SOA', 'PTR']
   end
 
+  # List types usually used in reverse zones.
   def self.reverse_records
     ['PTR', 'CNAME', 'TXT', 'NS', 'NAPTR']
   end
 
+  # List types that can be touched by a simple user.
   def self.allowed_record_types
     record_types - WebDNS.settings[:prohibit_records_types]
   end
 
   validates :name, presence: true
   validates :type, inclusion: { in: record_types }
 
   # http://mark.lindsey.name/2009/03/never-use-dns-ttl-of-zero-0.html
   validates_numericality_of :ttl,
                             allow_nil: true, # Default pdns TTL
                             only_integer: true,
                             greater_than: 0,
                             less_than_or_equal_to: 2_147_483_647
 
   # Don't allow the following actions on drop privileges mode
   validate :no_touching_for_slave_zones, if: -> { domain.slave? }
 
   validates_drop_privileges :type,
                             message: 'You cannot touch that record!',
                             unless: -> { Record.allowed_record_types.include?(type) }
   validates_drop_privileges :name,
                             message: 'You cannot touch top level NS records!',
                             if: -> { type == 'NS' && domain_record? }
 
   before_validation :guess_reverse_name
   before_validation :set_name
   after_save :update_zone_serial
   after_destroy :update_zone_serial
 
-
-  # Smart order a list of domains
+  # Smart sort a list of records.
+  #
+  # Order by:
+  # * Top level records
+  # * Record name
+  # * SOA
+  # * NS
+  # * Friendly type
+  # * Priority
+  # * Content
+  #
+  # records - The list of records to order.
+  #
+  # Returns the list sorted.
   def self.smart_order(records)
     records.sort_by { |r|
       [
         r.domain_record? ? 0 : 1,   # Zone records
         r.name,
         r.type == 'SOA' ? 0 : 1,
         r.type == 'NS' ? 0 : 1,
         record_types.index(r.type), # Friendly type
         r.prio,
         r.content
       ]
     }
   end
 
+  # Get the a short name for the record (without the zone suffix).
+  #
+  # Returns a string.
   def short
     return '' if name == domain.name
     return '' if name.blank?
 
     File.basename(name, ".#{domain.name}")
   end
 
+  # Returns true if this is a zone record.
   def domain_record?
     name.blank? || name == domain.name
   end
 
+  # Find out if the record is edittable.
+  #
+  # by - Editable by :user or :admin.
+  #
+  # Returns true if the record is editable.
   def editable?(by = :user)
     return false if domain.slave?
 
     case by
     when :user
       return false unless Record.allowed_record_types.include?(type)
       return false if type == 'NS' && domain_record?
     end
 
     true
   end
 
+  # Find out this record type supports priorities.
+  #
+  # We set this to false by default, record types that support priorities.
+  # shoule override this.
+  #
+  # Returns true this record type support priorities.
   def supports_prio?
     false
   end
 
-  # Create record specific urls for all record types
+  # Make sure rails generates record specific urls for all record types.
   #
-  # Overrides default rails STI
+  # Overrides default rails STI behavior.
   def self.model_name
     return super if self == Record
 
     Record.model_name
   end
 
+  # Generate the usual admin friendly DNS record line.
+  #
+  # Returns a string.
   def to_dns
     [name, ttl, 'IN', type, supports_prio? ? prio : nil, content].compact.join(' ')
   end
 
+  # Generate a shorter version of the DNS record line.
+  #
+  # Returns a string.
   def to_short_dns
     [name, 'IN', type].join(' ')
   end
 
   private
 
   # Validations
 
   def no_touching_for_slave_zones
     # Allow automatic SOA creation for slave zones
     # powerdns needs a valid serial to compare it with master
     return if type == 'SOA' && validation_context == :create
 
     errors.add(:type, 'This is a slave zone!')
   end
 
   # Hooks
 
   def guess_reverse_name
     return if not type == 'PTR'
     return if not domain.reverse?
     return if name.blank?
 
     reverse = IPAddr.new(name).reverse
     self.name = reverse if reverse.end_with?(domain.name)
   rescue IPAddr::InvalidAddressError # rubycop:disable HandleExceptions
   end
 
   # Powerdns expects full domain names
   def set_name
     self.name = domain.name if name.blank?
     self.name = "#{name}.#{domain.name}" if not name.end_with?(domain.name)
   end
 
   def remove_terminating_dot
     self.content = content.gsub(/\.+\Z/, '')
   end
 
   def update_zone_serial
     # SOA records handle serial themselves
     return true if type == 'SOA'
     return true if !domain
 
     domain.soa.bump_serial!
   end
 
 end
diff --git a/lib/bean/client.rb b/lib/bean/client.rb
index cbcb04f..136dd24 100644
--- a/lib/bean/client.rb
+++ b/lib/bean/client.rb
@@ -1,41 +1,52 @@
 module Bean
   class Client
+
+    # Initialize a new Bean::Client.
+    #
+    # host - The host to connect to.
+    #
     def initialize(host)
       @host = host
     end
 
+    # Put a job in the default beanstalk tube.
     def put(body)
       client.tubes['default'].put(body)
     rescue Beaneater::NotConnected
       reconnect!
     end
 
+    # Get a job from the dafault beanstalk tube.
     def reserve(*args)
       client.tubes.reserve(*args)
     end
 
+    # Reconnect to the beanstalk server.
+    #
+    # retries - Number of retries before failing.
+    # sleep_time - Time between retries.
     def reconnect!(retries = 3, sleep_time = 0.5)
       client!
     rescue Beaneater::NotConnected => exception
       retries -= 1
       raise exception if retries.zero?
       sleep(sleep_time)
       retry
     end
 
     private
 
     def client
       @client ||= client!
     end
 
     def client!
       @client.close if @client # rescue nil
       @client = connect
     end
 
     def connect
       Beaneater.new(@host)
     end
   end
 end
diff --git a/lib/bean/worker.rb b/lib/bean/worker.rb
index fa09118..61e7dd6 100644
--- a/lib/bean/worker.rb
+++ b/lib/bean/worker.rb
@@ -1,70 +1,77 @@
 require 'singleton'
 
 module Bean
   class Worker
     include Singleton
 
     TIMEOUT = 5
 
     attr_accessor :job
 
+    # Start consuming jobs.
     def self.work
       instance.work
     end
 
+    # Start consuming jobs.
+    #
+    # Handles reconnects.
     def work
       register_signals
       watch
     rescue Beaneater::NotConnected
       Base.beanstalk_reconnect!
     end
 
+    # Graceful stop the worker.
+    #
+    # If no job is running stops immediately.
     def stop
       if job.nil?
         exit
       else
         @stop = true
       end
     end
 
     private
 
     def stop? # rubocop:disable Style/TrivialAccessors
       @stop
     end
 
     def register_signals
       trap('INT') { stop }
       trap('TERM') { stop }
     end
 
     def watch
       loop do
         procline('watching')
         break if stop?
 
         process_job
       end
     rescue Beaneater::TimedOutError
       retry
     end
 
     def process_job
       self.job = Base.bean.reserve(TIMEOUT)
       log_job
 
       job.delete
     ensure
       self.job = nil
     end
 
     def log_job
       procline("working on jobid=#{job.id} #{job.body}")
       Rails.logger.warn(job_id: job.id, job_body: job.body.to_s)
     end
 
     def procline(line)
       $0 = "bean-#{line}"
     end
   end
 end
diff --git a/lib/drop_privileges_validator.rb b/lib/drop_privileges_validator.rb
index f735ee1..4a69826 100644
--- a/lib/drop_privileges_validator.rb
+++ b/lib/drop_privileges_validator.rb
@@ -1,27 +1,28 @@
 module ActiveModel
   module Validations
     class DropPrivilegesValidator < EachValidator
       def initialize(options)
         super
         setup!(options[:class])
       end
 
+      # Add an error on the specified attribute if we are on drop privileges mode.
       def validate_each(record, attribute, _value)
         record.errors.add(attribute, options[:message]) if record.drop_privileges
       end
 
       private
 
       def setup!(klass)
         klass.send(:attr_reader, :drop_privileges) unless klass.method_defined?(:drop_privileges)
         klass.send(:attr_writer, :drop_privileges) unless klass.method_defined?(:drop_privileges=)
       end
     end
 
     module HelperMethods
       def validates_drop_privileges(*attr_names)
         validates_with DropPrivilegesValidator, _merge_attributes(attr_names)
       end
     end
   end
 end
diff --git a/lib/ipv4_validator.rb b/lib/ipv4_validator.rb
index 55b4165..67e42ca 100644
--- a/lib/ipv4_validator.rb
+++ b/lib/ipv4_validator.rb
@@ -1,17 +1,19 @@
 require 'ipaddr'
 require 'socket'
 
 class Ipv4Validator < ActiveModel::EachValidator
+  # Returns true if addr is a valid IPv4 address.
   def valid_v4?(addr)
     return false if addr['/'] # IPAddr accepts addr/mask format
     IPAddr.new(addr, Socket::AF_INET)
     true
   rescue IPAddr::AddressFamilyError, IPAddr::InvalidAddressError
     false
   end
 
+  # Add an attribute error if this is not a valid IPv4 address.
   def validate_each(record, attribute, value)
     return if valid_v4?(value)
     record.errors[attribute] << 'is not a valid IPv4 address'
   end
 end
diff --git a/lib/ipv6_validator.rb b/lib/ipv6_validator.rb
index 09780c3..cdd1876 100644
--- a/lib/ipv6_validator.rb
+++ b/lib/ipv6_validator.rb
@@ -1,21 +1,22 @@
 require 'ipaddr'
 require 'socket'
 
 class Ipv6Validator < ActiveModel::EachValidator
-
+  # Returns true if addr is valid IPv6 address.
   def valid_v6?(addr)
     return false if addr['/'] # IPAddr accepts addr/mask format
     return false if addr['[']
     return false if addr[']']
     IPAddr.new(addr, Socket::AF_INET6)
     true
   rescue IPAddr::AddressFamilyError, IPAddr::InvalidAddressError
     false
   end
 
+  # Add an attribute error if this is not a valid IPv4 address.
   def validate_each(record, attribute, value)
     return if valid_v6?(value)
 
     record.errors[attribute] << 'is not a valid IPv6 address'
   end
 end
diff --git a/lib/notification.rb b/lib/notification.rb
index 43c401e..049be51 100644
--- a/lib/notification.rb
+++ b/lib/notification.rb
@@ -1,93 +1,93 @@
 require 'singleton'
 
 class Notification
   include Singleton
 
-  # Send out a notification about notable record changes
+  # Send out a notification about notable record changes.
   def notify_record(user, record, context)
     ActiveSupport::Notifications.instrument(
       'webdns.record',
       user: user,
       context: context,
       object: record)
   end
 
-  # Send out a notification about notable domain changes
+  # Send out a notification about notable domain changes.
   def notify_domain(user, domain, context)
     ActiveSupport::Notifications.instrument(
       'webdns.domain',
       user: user,
       context: context,
       object: domain)
   end
 
-  # Subscribe to domain/record notifications
+  # Subscribe to domain/record notifications.
   def hook
     hook_record
     hook_domain
   end
 
   private
 
   def hook_record
     ActiveSupport::Notifications
       .subscribe 'webdns.record' do |_name, _started, _finished, _unique_id, data|
       handle_record(data)
     end
   end
 
   def hook_domain
     ActiveSupport::Notifications
       .subscribe 'webdns.domain' do |_name, _started, _finished, _unique_id, data|
       handle_domain(data)
     end
   end
 
   def handle_record(data)
     record, context, user = data.values_at(:object, :context, :user)
     domain = record.domain
     changes = record.previous_changes
 
     # Nobody is interested in those
     changes.delete('updated_at')
     changes.delete('created_at')
     return if changes.empty?
 
     others = domain.group.users.where.not(id: user.id).pluck(:email)
     return if others.empty?
 
     admin_action = !user.groups.exists?(domain.group_id)
 
     NotificationMailer.notify_record(
       record: record,
       context: context,
       user: user,
       admin: admin_action,
       others: others,
       changes: changes
     ).deliver
   end
 
   def handle_domain(data)
     domain, context, user = data.values_at(:object, :context, :user)
     changes = domain.previous_changes
 
     # Nobody is interested in those
     changes.delete('updated_at')
     changes.delete('created_at')
     return if changes.empty?
 
     others = domain.group.users.where.not(id: user.id).pluck(:email)
     return if others.empty?
 
     admin_action = !user.groups.exists?(domain.group_id)
     NotificationMailer.notify_domain(
       domain: domain,
       context: context,
       user: user,
       admin: admin_action,
       others: others,
       changes: changes
     ).deliver
   end
 end
diff --git a/lib/prio_validator.rb b/lib/prio_validator.rb
index f2395b6..cd96596 100644
--- a/lib/prio_validator.rb
+++ b/lib/prio_validator.rb
@@ -1,18 +1,19 @@
-# Prio should be between [0, 65535]
+# Validates DNS priorities [0, 65535]
 class PrioValidator < ActiveModel::EachValidator
+  # Adds an attribute error if value is not a valid DNS priority.
   def validate_each(record, attribute, value)
     # Rails autocasts integer fields to 0 if a non-numerical value is passed
     # we override that by using th *_before_type_cast helper method
     before_cast = :"#{attribute}_before_type_cast"
     raw_value = record.send(before_cast) if record.respond_to?(before_cast)
     raw_value ||= value
 
     val = Integer(raw_value)
     if val < 0 || val > 65_535
       record.errors[attribute] << 'is not a valid DNS priority [0, 65535]'
     end
 
   rescue ArgumentError, TypeError
     record.errors[attribute] << 'is not a valid DNS priority [0, 65535]'
   end
 end
diff --git a/lib/strategies/date.rb b/lib/strategies/date.rb
index ed4e145..8031d79 100644
--- a/lib/strategies/date.rb
+++ b/lib/strategies/date.rb
@@ -1,19 +1,29 @@
 module Strategies
   module Date
     module_function
 
+    # Generate a new date based serial.
+    #
+    # A serial is generate based on the current date where 2 digits
+    # are held as a counter for changes that happen in the same date.
+    # We also make sure that the new serial is larger than the previous
+    # one.
+    #
+    # current_serial - The current zone zone serial.
+    #
+    # Returns the new serial.
     def generate_serial(current_serial)
       # Optimization for the case that current_serial is a lot larger
       # than the generated serial
       new = [
         Time.now.strftime('%Y%m%d00').to_i,
         current_serial
       ].max
 
       # Increment until we find a spot
       new += 1 while new <= current_serial
 
       new
     end
   end
 end
diff --git a/lib/strategies/incremental.rb b/lib/strategies/incremental.rb
index 75a4772..56ba86e 100644
--- a/lib/strategies/incremental.rb
+++ b/lib/strategies/incremental.rb
@@ -1,9 +1,12 @@
 module Strategies
   module Incremental
     module_function
 
+    # Generate a new incremental serial for the zone.
+    #
+    # Returns the new serial.
     def generate_serial(current_serial)
       current_serial + 1
     end
   end
 end