Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

If you know Rails or Sinatra

What carries over, what is spelled differently, and the four habits that will not work here.

melee’s surface was deliberately shaped like Sinatra, because that is the shape people and models already know. The differences are not stylistic — each one comes from the app being compiled ahead of time and each request being its own process.

The translation table

Rails / Sinatramelee
get "/x" do ... end (Sinatra)the same
params[:id]params[:id] (String or nil) or params.fetch(:id) (String, "" when absent)
session[:user_id] = 1session[:user_id] = "1" — session values are Strings
redirect_to "/x"redirect "/x" (always 303)
head :not_found / halt 404halt 404, or halt 400, "why"
render :index, locals: {a: 1}render :index, a: 1
render partial: "row", locals: {r: r}partial :row, r: r
before_action :require_adminbefore "/admin" do ... end — a path prefix, not a callback name
rescue_fromerror do |e| ... end
ActiveRecord::Basedb.query "SELECT ...", bind — SQL, no ORM
db/migrate/*.rbdb/migrations/NNN_name.sql — SQL files, applied in name order
app/views/x.html.erbviews/x.erb
<%= form_authenticity_token %><%= csrf_field %>
Rails.logger.info "x"log.info "x", key: value — structured, one event per call
ENV["SECRET"]ENV["SECRET"], set with melee env SECRET value
public/public/
bin/rails servermelee dev
ActiveJob / sidekiqa durable object with a timer — see Background jobs
whenever / cronthe same timer
Rails.cachethe database, or a durable object

The four habits that will not work

1. Nothing survives the request in memory

# Rails: fine. melee: the next request gets a fresh process and recomputes it.
def settings
  @settings ||= db.query("SELECT * FROM settings")
end

This is not an error — the memoisation works perfectly well within one request, and that is often what you wanted. What does not happen is the second request seeing it. A process-lifetime cache, a connection pool, a class-level registry filled at boot: all of these are per-request in melee.

Where you genuinely need something to persist, that is what a durable object is.

2. There is no background anything

No threads, no Thread.new, no job queue, no after_commit running later, no fork of your own. The request child answers and exits. Work that must happen outside a request happens in a durable object’s timer, and work that must happen because of a request either happens during it or gets recorded for a timer to pick up. Background jobs and scheduled work covers the patterns.

3. No gems, and no metaprogramming

The whole program is compiled ahead of time, so anything that decides what to call at run time is out: eval, method_missing, define_method with a computed name, send with a computed name, Class.new, ObjectSpace. Dispatch with case. The available requires are a fixed list — json, base64, digest, securerandom, uri, net/http, openssl, set, csv, strscan, optparse, pathname, tmpdir, forwardable — plus require_relative for your own files.

There are a few smaller surprises in the same family: string literals are frozen, there is no Date class and no Time.parse, and a rescue clause has to name its exception class in full rather than through a constant alias. The Ruby that compiles is the complete list with a workaround for each.

4. Compile-time checking is real but narrower than it looks

The build catches the shape of the program, which Rails would not: a template rendered with the wrong locals, a migration whose SQL does not parse, a require of something that is not there, a forbidden construct like eval, a call to a top-level helper you never defined.

It does not catch wrong arity, calling a method on nil, or any undefined method reached through an explicit receiverdb.frist(...), "x".nope and Household.get("home").refrsh all compile and raise NoMethodError when the line runs, however well the compiler knows the receiver’s type. So a green melee check is not the same as a working app: click through it under melee dev before you push.

What is better than you expect

  • The error page. Under melee dev, a 500 shows the exception, the request, the params, the session and a backtrace. In production there is no backtrace at all (Spinel does not have them), but there is a structured log line — see When something breaks.
  • SQLite is enough. It is embedded in the binary, it is on the local disk, and a query is a function call rather than a network round trip. For the size of app melee is for, this removes most of what a database usually costs you.
  • Deploys are atomic and instant. A new release is a new directory and a symlink swap. A failed build changes nothing.
  • You can read the whole standard library. It is a closed, documented surface — the API reference is generated from the type signatures, so it is exactly what exists.

Next