Project

sequel-d1

0.0
The project is in a healthy, maintained state
Sequel `:d1` adapter and Opal compatibility patches for Cloudflare D1. Pass a duck-typed D1 binding to `Sequel.connect(adapter: :d1, d1: binding)`. Includes the `homura db:migrate:*` tooling to compile Sequel migration DSL to wrangler SQL.
2005
2006
2007
2008
2009
2010
2011
2012
2013
2014
2015
2016
2017
2018
2019
2020
2021
2022
2023
2024
2025
2026
 Dependencies

Runtime

= 1.8.3.rc1.5
~> 5.0
~> 2.0
 Project Readme

homura mascot

homura

A platform for running real Ruby applications on Cloudflare Workers.

Live site: https://homura.kazu-san.workers.dev

⚠️ Status — active development. Do not use in production yet. homura is pre-1.0 and changes hard between releases. Public surfaces (gem APIs, CLI, scaffolder layout, build artifacts) can shift in a breaking way at any 0.x bump. Pin exact gem versions if you experiment, expect to chase the next minor, and treat the live demos at *.kazu-san.workers.dev as dogfooding fixtures rather than SLA targets.

# app.rb — yes, it really is plain Sinatra.
require 'sinatra'
require 'sequel'

get '/' do
  'Hello from Ruby, running on Cloudflare Workers.'
end

get '/users' do
  db = Sequel.connect(adapter: :d1, d1: d1)
  content_type :json
  db[:users].order(:id).all.to_json
end
# wrangler.toml — exactly what Cloudflare expects.
name = "myapp"
main = "build/worker.entrypoint.mjs"
compatibility_date = "2026-04-27"
compatibility_flags = ["nodejs_compat"]

There is no shim, no transpiled Sinatra-lookalike. require 'sinatra' loads the canonical Sinatra port; redirect '/' raises Sinatra's own HaltResponse; erb :index, locals: { user: u } resolves locals the way the docs say it does. Modular apps use the same class App < Sinatra::Base

  • config.ru + run App shape upstream documents — see examples/todo/ and the canonical site/ homura.kazu-san.workers.dev source. The whole pipeline exists so Ruby people can keep writing Ruby and ship it to the Cloudflare edge.

Rack-only quick start

Sinatra is optional. A plain Rack app can run directly on the same Workers adapter:

# config.ru
run ->(env) {
  [
    200,
    { 'content-type' => 'text/plain; charset=utf-8' },
    ["Hello from Rack on Cloudflare Workers\n"]
  ]
}

See examples/rack/ for the deployed Rack-only fixture.


Why homura

Cloudflare Workers does not run a Ruby VM. It runs JavaScript on V8, with no filesystem, no eval-from-string, no native extensions. The standard Ruby stack — Sinatra, Sequel, SecureRandom, ERB — assumes all of those.

homura is the glue that closes that gap:

  • Real Sinatra DSLget, post, before, after, helpers, halt, redirect, erb with locals: and layout, Sinatra::Base inheritance exactly as upstream documents.
  • Edge SQLSequel.connect(adapter: :d1, d1: d1) works against Cloudflare D1; migrations are written in Sequel DSL and compiled to wrangler-compatible SQL. Or skip the ORM and use the db.execute(...) wrapper directly.
  • All the bindings — D1, KV, R2, Workers AI, Queues, Scheduled, Durable Objects, Vectorize. They surface as ordinary Ruby helpers such as db, d1, kv, bucket, ai, send_email, jobs_queue, and durable_object(:counter, 'global').
  • Ordinary distribution — four gems on RubyGems. No path:, no submodules, no clone of homura required.
  • Async without ceremony — the build step absorbs Workers' promise-shaped binding calls so user source stays sync-looking, like the Ruby you would write on CRuby. db.execute(...) reads like sqlite3-ruby, not like a coroutine.

If a stock Sinatra-on-Workers idiom doesn't behave the way Ruby developers expect, that is a bug in homura, not a quirk of the stack. The examples/ directory is the contract.


How it works

                         CRuby (build host)               Cloudflare Workers (V8)
                ┌────────────────────────────────┐        ┌──────────────────────────┐
 your Ruby ───► │  bundle exec rake build         │ ─────► │   build/worker.entrypoint.mjs  │
 your views ───►│   ├─ Opal compile (Ruby → JS)   │        │   (loaded by wrangler)   │
 your migrate ─►│   ├─ ERB precompile             │        │   ├─ homura runtime      │
                │   ├─ public/ asset embed        │        │   ├─ your compiled app   │
                │   └─ auto-await pass            │        │   └─ binding shims       │
                └────────────────────────────────┘        └──────────────────────────┘

Four gems own the work:

Gem Responsibility
opal-homura Patched Opal compiler — turns Ruby into the JavaScript that V8 runs. Keep require: 'opal'.
homura-runtime Worker entrypoint, Rack adapter, Cloudflare binding wrappers (D1/KV/R2/AI/Queue), build pipeline (homura build).
sinatra-homura Sinatra port + Opal-compatibility patches, scaffolder (homura new), JWT / Scheduled / Queue helpers, ERB precompiler.
sequel-d1 Sequel adapter for Cloudflare D1, migration compiler (homura db:migrate:*).

The build output is a single build/worker.entrypoint.mjs plus the embedded asset bundle. wrangler deploy ships that file straight to the edge.


Quick start: a new project

Prerequisites: Ruby 3.4+, Node 20+, wrangler (or just npx wrangler).

# Install the scaffolder.
gem install sinatra-homura

# Generate a project (add --with-db for D1 + Sequel).
homura new myapp
cd myapp

# Install dependencies.
bundle install
npm install

# Build the Worker bundle.
bundle exec rake build

# Run wrangler dev locally.
bundle exec rake dev
# → http://127.0.0.1:8787

homura new --with-db myapp additionally writes a Sequel migration under db/migrate/, declares a D1 binding in wrangler.toml, and adds db:migrate:compile, db:migrate:local, and db:migrate:remote Rake tasks. The local development cycle is:

bundle exec rake db:migrate:local   # apply migrations to local D1 (sqlite shim)
bundle exec rake build              # rebuild after editing Ruby
bundle exec rake dev                # wrangler dev, hot-reloads on rebuild

Production deploy:

npx wrangler d1 create myapp                       # one-time
# paste the new database_id into wrangler.toml
bundle exec rake db:migrate:remote                 # apply to remote D1
bundle exec rake deploy                            # wrangler deploy

Adding homura to an existing project

If you already have Ruby and want to ship it to Workers:

  1. Pin the four gems in your Gemfile:

    source 'https://rubygems.org'
    ruby '>= 3.4.0'
    
    gem 'rake'
    gem 'opal-homura',    '= 1.8.3.rc1.5', require: 'opal'
    gem 'homura-runtime', '~> 0.3'
    gem 'sinatra-homura', '~> 0.3'
    gem 'sequel-d1',      '~> 0.3'   # only if you want D1 / Sequel
  2. Keep your Sinatra app exactly as you wrote it. Classic style (require 'sinatra' + top-level get '/' do ... end) and modular style (require 'sinatra/base' + class App < Sinatra::Base + config.ru with run App) both work as upstream documents. You do not swap the require line for a Cloudflare-flavoured one — sinatra-homura wires the Workers runtime into the standard Sinatra entry points automatically.

  3. Add wrangler.toml pointing main at build/worker.entrypoint.mjs and declaring the bindings you need (D1 / KV / R2 / AI / Queue).

  4. Build and run with bundle exec rake build && bundle exec rake dev.

For an existing classic-style Sinatra app, the smallest path is to copy examples/classic-top-sinatra/ and merge your routes in. For a modular-style app, start from examples/todo/ (no ORM) or examples/todo-orm/ (Sequel + D1).

The Workers gotchas section below lists the small set of places where the Workers runtime forces a divergence from CRuby.


Examples

examples/ contains twelve fully-working applications, each one a standalone project that depends on the published gems only — no path: references back to the monorepo. They are also the regression fixtures behind the latest gem releases.

Example Live What it shows
sinatra https://sinatra.kazu-san.workers.dev/ The classic Sinatra README snippet — require 'sinatra' + get '/frank-says'. Single app.rb, no D1, no views.
rack https://rack.kazu-san.workers.dev/ Direct Rack response triples with run ->(env) { ... }; no Sinatra require.
classic-top-sinatra https://classic-top-sinatra.kazu-san.workers.dev/ Same shape as sinatra but with content_type :json + a JSON route, to dogfood the classic top-level DSL.
sinatra-with-db https://sinatra-with-db.kazu-san.workers.dev/ Smallest D1-backed Sinatra: Sequel.connect(adapter: :d1, d1: d1), one route, one migration.
sinatra-with-email https://sinatra-with-email.kazu-san.workers.dev/ Phase 17.5 auto-await demo — POST /send over the SEND_EMAIL Cloudflare Email binding with sync-shaped Ruby source.
todo-simple https://todo-simple.kazu-san.workers.dev/ The smallest stateful example. One app.rb, no views/, no D1 — HTML written as Ruby heredocs. The thing to copy when "how little does homura need" is the question.
todo https://todo.kazu-san.workers.dev/ D1-backed CRUD without an ORM — db.execute / db.execute_insert directly.
todo-orm https://todo-orm.kazu-san.workers.dev/ The same TODO app, this time through sequel-d1: migrations, dataset chains, .first / .update.
auth-otp https://auth-otp.kazu-san.workers.dev/login Email OTP login. Sends through mailpit in development; HMAC-signed session cookie; full headed Playwright E2E in rake e2e:headed.
blog https://blog.kazu-san.workers.dev/ A small blog: index / detail / new / proper 404 / delete. Demonstrates async-route status preservation and <%= h(post[:body]).gsub("\n", "<br>") %>.
inertia-todo https://inertia-todo.kazu-san.workers.dev/ A thin SPA via Inertia.js + Vue 3, with Sinatra serving page props. Client-side JS lives in public/assets/.
hotwire-todo https://hotwire-todo.kazu-san.workers.dev/ Turbo Streams (server-rendered partials over Accept negotiation) + a tiny Stimulus controller for autofocus.

See examples/README.md for the full index with URLs and per-app feature notes.


Known gotchas (and where they are handled)

The Workers runtime forces a few real divergences from CRuby. homura absorbs each of them in the gem layer so application code does not have to:

  • No String#<<. Opal Strings are JS strings, so any code that builds output through mutation (host = String.new; host << '...') breaks. sinatra-homura rebuilds Sinatra::Helpers#uri / redirect / content_type with +-style concatenation, so redirect to('/') works the way Sinatra docs say.
  • No binding.eval. Workers blocks new Function(string), which ERB uses for template rendering. homura-runtime precompiles every views/*.erb to a Ruby method at build time and dispatches erb :name there. erb :_partial, locals: { t: t } resolves bare t via a template-locals stack + Sinatra::Base#method_missing.
  • No filesystem. public/ is bundled at build time and served from memory by a Rack middleware homura installs.
  • String == Symbol under Opal. Sequel::LiteralString was being caught by Sequel's case v when Symbol branch and emitted as a backtick-quoted identifier. sequel-d1 reorders the literal_append branches so update(done: Sequel.lit('1 - done')) lands in literal_literal_string_append.
  • Async at the edge. D1 / KV / fetch are all promise-shaped. The build pipeline runs an auto-await pass for registered async methods (db.execute, kv.get, ai.run, …), so route bodies stay sync-shaped.

If you find an idiom that should "just work" but does not, it belongs on this list. File an issue.


AI / agent support

homura ships agent-discoverable docs so Claude / Copilot / Cursor can pick the right gem and follow the canonical install/build flow without a human in the loop.

Install the skill:

# GitHub Copilot (or any host that reads `gh skill`)
gh skill install kazuph/homura homura-workers-gems --agent github-copilot --scope user

# Claude Code (project-scoped is recommended)
gh skill install kazuph/homura homura-workers-gems --agent claude-code --scope project

# npm-based installer (also works for Claude / Copilot)
npx skills add kazuph/homura --skill homura-workers-gems -a claude-code

Repository layout

gems/                 # The four published gems live here
  homura-runtime/     # core runtime + build pipeline
  sinatra-homura/     # Sinatra port + Opal patches + scaffolder
  sequel-d1/          # Sequel D1 adapter + migration compiler
  sinatra-inertia/    # Inertia.js v2 adapter for Sinatra
vendor/opal-gem/      # Source of opal-homura (the patched Opal fork)
vendor/                 # Bundled Sinatra / Rack / Sequel / etc. for Workers
examples/             # Standalone example apps consuming the released gems
site/                 # The canonical homura.kazu-san.workers.dev application
                      # (Sinatra app, ERB views, public/, wrangler.toml)
test/                 # Gem-side smoke + Ruby tests (`npm test`)
docs/                 # Long-form documentation
skills/               # Installable agent skills

The gems/* directories and vendor/opal-gem/ are the shipping surface. site/ is the dogfooding application that proves the gems work end-to-end on Cloudflare Workers; it lives in this repo as a sibling of the gem code so runtime regressions show up the moment they ship.


Repo maintenance

Tracked first-party Ruby is mechanically formatted with fables-tales/rubyfmt, the same formatter lineage Stripe describes in its rubyfmt rollout story. Vendored and generated paths stay outside that boundary.

npm run format:ruby:install
npm run format:ruby
npm run format:ruby:check
ruby bin/install-git-hooks

The formatter scope is defined once in bin/first-party-ruby-files and shared by the formatter and ruby -c CI gates.

ruby bin/install-rubyfmt downloads the pinned rubyfmt release used by CI into build/tools/. If you already manage rubyfmt globally (for example with brew install rubyfmt), bin/format-ruby will use that too.

ruby bin/install-git-hooks configures core.hooksPath=.githooks for the current clone. The pre-commit hook auto-formats staged first-party Ruby so local commits fail earlier, while CI remains the source of truth.


License

MIT. See LICENSE.