cloudflare-ruby
Ruby SDK for the Cloudflare API. Hand-written, narrow on purpose — currently covers the RealtimeKit product surface; other surfaces (R2, Workers, DNS) added on demand.
Status: prototyping. API not yet stable. The 0.0.x line is exploratory; the first stable release will be
0.1.0. Repo is private until then.
Product surfaces
Each Cloudflare product is its own namespace under Cloudflare::, with its own README living next to the code:
| Product | Namespace | README |
|---|---|---|
| RealtimeKit | Cloudflare::RealtimeKit |
lib/cloudflare/realtime_kit/README.md |
New product surfaces are added by writing the resource classes by hand (no codegen) and adding a slice entry under spec/slices/<product>.json for drift detection.
Why hand-rolled and not openapi-generator / Stainless
- We only need a thin slice of Cloudflare's 2,000+ endpoint API.
- We want code that reads like the rest of our Ruby — generated SDKs in any language carry a generic style we'd rather not adopt.
- A dedicated drift checker, scoped to what we use, gives us automation without 17 KLOC of generated noise.
See the design doc for the full reasoning.
Install
# Gemfile
gem "cloudflare-ruby", git: "git@github.com:tokimonki/cloudflare-ruby.git"
Configuration
Stripe-style global config; every resource picks it up implicitly.
Cloudflare.configure do |c|
c.api_token = ENV.fetch("CLOUDFLARE_API_TOKEN")
c.account_id = ENV.fetch("CLOUDFLARE_ACCOUNT_ID") # default for every call
end
Per-call account_id: overrides the global. api_token has no per-call override — set it once.
Errors
Every product surface raises a member of the same hierarchy:
begin
Cloudflare::RealtimeKit::Meeting.find("missing", app_id: "app-1")
rescue Cloudflare::NotFoundError => e
e.status # => 404
e.body # => parsed response body
end
Hierarchy:
Cloudflare::Error
├── AuthenticationError (401, 403)
├── NotFoundError (404)
├── ValidationError (422)
├── RateLimitError (429)
└── ServerError (5xx)
Connection retries 429/502/503/504 up to 2× with backoff before raising.
Drift detection
The SDK is hand-written against a saved slice of Cloudflare's OpenAPI spec (spec/slices/<product>.json). When upstream changes, you re-extract the slice and diff to surface what moved:
bundle exec exe/cloudflare-ruby check-drift realtime_kit
# → "No drift detected for realtime_kit." (exit 0)
# → or a structured report of added/removed/changed paths and components (exit 1)
The fetched upstream spec is cached at tmp/upstream-openapi.json (gitignored) for inspection or offline re-runs (--spec PATH).
To re-baseline after an intentional update:
bundle exec exe/cloudflare-ruby refresh-slice realtime_kit
The diff covers paths plus every components.* sub-section discovered in either slice's union — new sections upstream starts using (e.g., requestBodies, securitySchemes) get diffed without code changes.
Architecture
lib/
├── cloudflare.rb # top-level module, autoload registry
└── cloudflare/
├── version.rb
├── configuration.rb # global config (api_token, account_id, base_url)
├── connection.rb # Faraday singleton — auth, retries, JSON
├── errors.rb # status-keyed error classes
├── resource.rb # AR-flavored base (attribute, has_many, has_one, ...)
├── relation.rb # nested-collection proxy
└── <product>/ # one folder per product surface
├── README.md # product-specific DX guide
└── *.rb # resource classes
codegen/ # drift detection, not codegen
├── cli.rb # Thor: refresh-slice + check-drift
├── slicer.rb # extract paths-by-prefix from upstream
├── drift_detector.rb # structured diff
└── spec_version.rb # OpenAPI 3.0.3 pin
spec/slices/<product>.json # the saved slice — drift baseline
test/ # foundation + per-resource tests
Development
bundle install
bundle exec rake test
Adding a new product surface:
bundle exec exe/cloudflare-ruby refresh-slice <product>(after registering it incodegen/cli.rbPRODUCTS).- Write the resource classes under
lib/cloudflare/<product>/. - Write a
lib/cloudflare/<product>/README.mddocumenting the DX. - Add a row to the product surfaces table above.
License
MIT.