Jamie Lawrence

Ferry

A family logistics app for a household of four. It turns the kids' school, climbing, volleyball and bus runs into trips that exactly one parent owns, or that are visibly unowned, and shows only what needs a decision.

Visit the app ↗ · Private repo

Built with: Ruby · melee · Spinel · SQLite · Durable objects · Google Calendar · Web Push · PWA

Existing family calendar apps just put everyone’s calendar in one place. That’s just a mess of unnecessary information: I don’t need to know what my kids’ classes are; the kids don’t need to know what meetings I have. What we really need to know is where one person’s plans create an obligation for someone else, and whether that obligation is covered.

In other words, a family ride-sharing service.

Ferry is not a shared calendar. The calendars stay private. What it shares is the obligations: every school run, climbing session and volleyball match becomes a trip that someone owns, or that is visibly unowned (“get the bus!”). Routine fades into the background and only the deviations surface: an ask from my wife to cover a school run she normally does, an unclaimed trip, a change, a conflict, or a kid asking for a lift on a random night out.

How it was built

It runs on melee, compiled to a native binary by Spinel and installed on our phones as a PWA with a bottom tab bar. Server-rendered ERB, one stylesheet, a few dozen lines of JavaScript, no gems, no client framework.

Everything that outlives a request is a durable object with a timer. Each trip is a Job, a small state machine that walks a list of milestones: reminders, escalation to the other parent, offer expiry, the automatic switch to the bus, auto-done, and retirement a week later. melee gives an object exactly one timer, so six schedules became a set of flags and a single loop that runs whatever is due and then arms the timer for the next thing.

def on_timer
  return if storage.get("created").nil?
  steps = 0
  while steps < STEP_LIMIT
    name = next_milestone
    break if name.empty?
    now = Time.now.to_i
    break if milestone_at(name) > now
    return if run_milestone(name, now) == "retired"
    # A milestone that moved T (the automatic switch to the bus) is due again later, for the new T.
    flag(name) if milestone_at(name) <= now
    steps += 1
  end
  write_index_only
  arm
end

A Sweeper object is the household clock: it materialises the routine trips, flushes notifications and polls the parents’ free/busy. A CalendarWatch per kid syncs their Google Calendar through push channels, but reduces each event to a time, a place and a fingerprint. Titles are never stored, and a test greps every table for the fixture titles to prove it. The parents’ calendars are only ever read as free/busy. Web Push, with its RFC 8291 encryption and VAPID signing, is done in Ruby on the compiled OpenSSL.

10,678 lines all in, with 128 test cases that run under both CRuby and the compiled binary. I wrote the spec first, then one long Claude Code session built it across about fifteen subagents in parallel worktrees, over five working days.