ru.Bee is a Ruby-based web framework designed to streamline the development of modular monolith web applications. Under the hood, it leverages the power of Ruby and Rack backed by Puma, offering a clean, efficient, and flexible architecture. It offers a structured approach to building scalable, maintainable, and React-ready projects, making it an ideal choice for developers seeking a balance between monolithic simplicity and modular flexibility.
Want to get a quick API server up and running? You can do it in no time!
Starting from ru.Bee 2.0.0, ru.Bee supports WebSocket, which allows you to build real-time applications with ease.
Take a look at the ru.Bee demo site with full documentation: https://rubee.dedyn.io/ Want to explore how it was built? https://github.com/nucleom42/rubee-site
wrk -t4 -c100 -d30s https://rubee.dedyn.io/docs
Running 30s test @ https://rubee.dedyn.io/docs
4 threads and 100 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 304.95ms 33.22ms 551.86ms 90.38%
Req/Sec 82.25 42.37 280.00 69.86%
9721 requests in 30.02s, 4.11MB read
Requests/sec: 323.78
Transfer/sec: 140.07KBShort output explanation:
- Requests/sec: ~324
- Average latency: ~305 ms
- Total requests handled: 9,721
- Hardware: Raspberry Pi 5 (8 GB) — single board computer
- Server: ru.Bee app hosted via Nginx + HTTPS
This demonstrates ru.Bee's efficient architecture and suitability for lightweight deployments — even on low-power hardware.
Here is a short web frameworks comparison built with Ruby, so you can evaluate your choice with ru.Bee.
Disclaimer: The comparison is based on generic and subjective information available on the internet and is not a real benchmark. It is aimed at giving you a general idea of the differences between the frameworks and is not intended as a direct comparison.
| Feature / Framework | ru.Bee | Rails | Sinatra | Hanami | Padrino | Grape |
|---|---|---|---|---|---|---|
| React readiness | Built-in React integration (route generator can scaffold React components that fetch data via controllers) | React via webpacker/importmap, but indirect | No direct React support | Can integrate React | Can integrate via JS pipelines | API-focused, no React support |
| Routing style | Explicit, file-based routes with clear JSON/HTML handling | DSL, routes often implicit inside controllers | Explicit DSL, inline in code | Declarative DSL | Rails-like DSL | API-oriented DSL |
| Modularity | Lightweight core, pluggable projects | One project by default, but can be extended with respective gem | Very modular (small DSL) | Designed for modularity | Semi-modular, still Rails-like | Modular (mount APIs) |
| Startup / Load speed | Very fast (minimal boot time, designed for modern Ruby) | Not very fast, especially on large apps | Very fast | Medium (slower than Sinatra, faster than Rails) | Similar to Rails (heavier) | Fast |
| Ecosystem | Early-stage, focused on modern simplicity, but easily expandable via Bundler | Huge ecosystem, gems, community | Large ecosystem, many gems work | Small, growing | Small, less active | Small, niche |
| Learning curve | Simple, explicit, minimal DSL | Steep (lots of conventions & magic) | Very low (DSL fits in one file) | Medium, more concepts (repositories, entities) | Similar to Rails, easier in parts | Low (API-only) |
| Customizability | High (explicit over implicit, hooks & generators) | Limited without monkey-patching | Very high (you control flow) | High, modular architecture | Medium | High (designed for APIs) |
| Target use case | Modern full-stack apps with React frontends or APIs; well-suited if you prefer modular monolith over microservices | Large, full-stack, mature apps | Small apps, microservices | Modular apps, DDD | Rails-like but modular | APIs & microservices |
| Early adopters support | Personal early adopters support via fast extending and fixing | Not available | Not known | Not known | Not known | Not known |
- Installation
- Run tests
- Draw contract
- Model
- Routing
- Database
- Views
- Object hooks
- Validations
- JWT based authentication
- OAuth authentication
- ru.Bee commands
- Generate commands
- Migration commands
- ru.Bee console
- Rubee::Support
- Testing
- Background jobs
- Modular application
- Logger
- WebSocket
- Bee assistant
You can read the full docs on the demo site: rubee.dedyn.io
Lightweight – A minimal footprint focused on serving Ruby applications efficiently.
Modular – A modular approach to application development. Build a modular monolith app with ease by attaching as many subprojects as you need.
Contract-driven – Define your API contracts in a simple, declarative way, then generate all the boilerplate you need.
Fast – Optimized for speed, providing quick responses.
Rack-powered – Built on Rack. The full Rack API is available for easy integration.
Databases – Supports SQLite3, PostgreSQL, MySQL, and more via the Sequel gem.
Views – JSON, ERB, and plain HTML out of the box.
React Ready – React is supported as a first-class ru.Bee view engine.
Bundlable – Charge your ru.Bee app with any gem you need. Update effortlessly via Bundler.
ORM-agnostic – Models are native ORM objects, but you can use them as blueprints for any data source.
Authenticatable – Easily add JWT authentication to any controller action.
Hooks – Add logic before, after, or around any controller action.
Testable – Run all or selected tests using fast, beloved Minitest.
Asyncable – Plug in async adapters and use any popular background job engine.
Console – Start an interactive console and reload on the fly.
Background Jobs – Schedule and process background jobs using your preferred async stack.
WebSocket – Serve and handle WebSocket connections.
Logger – Use any logger you want.
- Install ru.Bee
gem install ru.Bee- Create your first project
rubee project my_project
cd my_project- Install dependencies
Prerequisites: make sure Ruby (3.1 or higher, 3.4.1 recommended) and Bundler are installed.
bundle install- Run the ru.Bee server. Default port is 7000.
rubee start # or rubee start_dev for development
# Starting from version 1.8.0, you can also start the server with the yjit compiler for a speed boost.
rubee start --jit=yjit
# This option is available for the dev environment too.
rubee start_dev --jit=yjit- Open your browser and go to http://localhost:7000
rubee test
# or specify a specific test file
rubee test models/user_model_test.rb
# or run a specific line in the test file
rubee test models/user_model_test.rb --line=12-
Add the routes to
routes.rbRubee::Router.draw do |router| ... # draw the contract router.get "/apples", to: "apples#index", model: { name: "apple", attributes: [ { name: 'id', type: :primary }, { name: 'colour', type: :string }, { name: 'weight', type: :integer }, { name: 'created', type: :datetime }, { name: 'updated', type: :datetime }, ] } end
-
Generate the files
rubee generate get /applesThis will generate the following files:
./app/controllers/apples_controller.rb # Controller with respective action
./app/views/apples_index.erb # ERB view rendered by the controller
./app/models/apple.rb # Model that acts as ORM
./db/create_apples.rb # Database migration file for the respective table- Run the initial database migration
rubee db run:all-
Fill the generated files with the logic you need and run the server again.
-
You can find a full snapshot of the schema in the
STRUCTUREconstant or in thedb/structure.rbfile. -
Print the latest schema from the
STRUCTUREconstant via the CLI
-> rubee db schema
--- users
- id, (PK), type (INTEGER)
- email, type (varchar(255))
- password, type (varchar(255))
--- accounts
- id, (PK), type (INTEGER)
- address, type (varchar(255))
- user_id, type (INTEGER)
--- posts
- id, (PK), type (INTEGER)
- user_id, type (INTEGER)
- comment_id, type (INTEGER)
--- comments
- id, (PK), type (INTEGER)
- text, type (varchar(255))
- user_id, type (INTEGER)- Print the schema for a specific table
-> rubee db schema posts
--- posts
- id, (PK), type (INTEGER)
- user_id, type (INTEGER), nullable
- comment_id, type (INTEGER), nullable
- created, type (datetime), nullable
- updated, type (datetime), nullable
Foreign keys:
- comment_id → comments() on delete no_action on update no_action
- user_id → users() on delete no_action on update no_action- Dropping all tables can be handy during development. Be careful and make sure you pass the desired environment.
RACK_ENV=test rubee db drop_tables
These tables have been dropped for the test env:
[:companies, :company_clients, :services]A model in ru.Bee is a simple Ruby object that can be serialized in the view in whatever form is required (e.g. JSON). Here is a simple example of rendering JSON from an in-memory object:
# ApplesController
def show
# In-memory example
apples = [Apple.new(colour: 'red', weight: '1lb'), Apple.new(colour: 'green', weight: '1lb')]
apple = apples.find { |apple| apple.colour = params[:colour] }
response_with object: apple, type: :json
endMake sure the Serializable module is included in the target class:
class Apple
include Serializable
attr_accessor :id, :colour, :weight
endYou can also turn it into an ORM object by extending Rubee::SequelObject, which is already serializable and charged with hooks:
class Apple < Rubee::SequelObject
attr_accessor :id, :colour, :weight
endIn the controller, query your target object directly:
# ApplesController
def show
apple = Apple.where(colour: params[:colour])&.last
if apple
response_with object: apple, type: :json
else
response_with object: { error: "apple with colour #{params[:colour]} not found" }, status: 422, type: :json
end
endInitiate a new record in memory
irb(main):015> user = User.new(email: "llo@ok.com", password: 543)
=> #<User:0x000000010cda23b8 @email="llo@ok.com", @password=543>Save a record to the database
irb(main):018> user.save
=> trueUpdate a record with a new value
irb(main):019> user.update(email: "update@email.com")
=> #<User:0x000000010c39b298 @email="update@email.com", @id=3, @password="543", @created="2025-09-28 22:03:07.011332 -0400", @updated="2025-09-28 22:03:07.011332 -0400">Check whether a record has been persisted
irb(main):016> user.persisted?
=> falseGet a record from the database and reload it
irb(main):011> user = User.last
=> #<User:0x000000010ccea178 @email="ok23@ok.com", @id=2, @password="123", ...>
irb(main):012> user.email = "new@ok.com"
=> "new@ok.com"
irb(main):014> user.reload
=> #<User:0x000000010c488548 @email="ok23@ok.com", @id=2, @password="123", ...> # unpersisted data refreshed from dbAssign attributes without persisting to the database
irb(main):008> User.last.assign_attributes(email: "bb@ok.com")
=> {"id" => 2, "email" => "ok23@ok.com", "password" => "123"}Get all records scoped by a field
irb(main):005> User.where(email: "ok23@ok.com")
=> [#<User:0x000000010cfaa5c0 @email="ok23@ok.com", @id=2, @password="123">]Get all records
irb(main):001> User.all
=> [#<User:0x000000010c239a30 @email="ok@ok.com", @id=1, @password="password", ...>]Find by id
irb(main):002> user = User.find 1
=> #<User:0x000000010c2f7cd8 @email="ok@ok.com", @id=1, @password="password", ...>Get the last record
irb(main):003> User.last
=> #<User:0x000000010c2f7cd8 @email="ok@ok.com", @id=1, @password="password", ...>Create a new persisted record
irb(main):004> User.create(email: "ok23@ok.com", password: 123)
=> #<User:0x000000010c393818 @email="ok23@ok.com", @id=2, @password=123, ...>Destroy a record and all related records
irb(main):021> user.destroy(cascade: true)
=> 1Find a record in the database or initialize a new instance for subsequent persistence
irb(main):020> user = User.find_or_new(email: "ok23@ok.com")
=> #<User:0x000000010cfaa5c0 @email="ok23@ok.com", @id=2, @password="123">
irb(main):021> user.persisted?
=> true
irb(main):022> user = User.find_or_new(email: "new@ok.com")
=> #<User:0x000000010cfaa5c0 @email="new@ok.com", @id=nil, @password=nil>
irb(main):023> user.persisted?
=> falseDestroy all records one by one
irb(main):022> User.destroy_all
=> [#<User ...>, #<User ...>]
irb(main):023> User.all
=> []Use complex query chains and serialize results back to ru.Bee objects in a single query:
# user model
class User < Rubee::SequelObject
attr_accessor :id, :email, :password, :created, :updated
owns_many :comments, over: :posts
end
# comment model
class Comment < Rubee::SequelObject
attr_accessor :id, :text, :user_id, :created, :updated
owns_many :users, over: :posts
end
# join post model
class Post < Rubee::SequelObject
attr_accessor :id, :user_id, :comment_id, :created, :updated
holds :comment
holds :user
endirb(main):008> result = Comment.dataset.join(:posts, comment_id: :id)
irb(main):009> .where(comment_id: Comment.where(text: "test").last.id)
irb(main):010> .then { |dataset| Comment.serialize(dataset) }
=> [#<Comment:0x0000000121889998 @id=30, @text="test", @user_id=702, ...>]Since version 2.6.0, Rubee::SequelObject supports chained queries. Supported methods: where, order, limit, offset, all, owns_many, owns_one, join, paginate.
irb(main):001> Comment.where(text: "test").where(user_id: 1)
=> [#<Comment:0x0000000121889998 @id=30, @text="test", @user_id=702, ...>]A paginate method is also available:
irb(main):001> comments = Comment.all.paginate(page: 1, per_page: 3)
irb(main):001> comments.pagination_meta
=> {:current_page=>1, :per_page=>3, :total_count=>10, :first_page=>true, :last_page=>false, :prev=>nil, :next=>2}ru.Bee supports Postgres and SQLite databases fully and can potentially be used with any database supported by the Sequel gem.
When using SQLite, include sqlite3 in your Gemfile:
gem 'sqlite3'Define your database URLs for each environment in config/base_configuration.rb:
Rubee::Configuration.setup(env = :development) do |config|
config.database_url = { url: 'sqlite://db/development.db', env: }
...
end
Rubee::Configuration.setup(env = :test) do |config|
config.database_url = { url: 'sqlite://db/test.db', env: }
...
end
Rubee::Configuration.setup(env = :production) do |config|
config.database_url = { url: 'sqlite://db/production.db', env: }
...
endFor PostgreSQL, include the pg gem and configure the URLs:
gem 'pg'Rubee::Configuration.setup(env = :development) do |config|
config.database_url = { url: "postgres://postgres@localhost:5432/development", env: }
...
end
Rubee::Configuration.setup(env = :test) do |config|
config.database_url = { url: "postgres://postgres@localhost:5432/test", env: }
...
end
Rubee::Configuration.setup(env = :production) do |config|
config.database_url = { url: "postgres://postgres:#{ENV['DB_PASSWORD']}@localhost:5432/production", env: }
...
endBefore starting the server or running the test suite, ensure your database is initialized:
rubee db init # ensures your database is created for each environment
RACK_ENV=test rubee db run:all # runs all migrations for the test environment
RACK_ENV=development rubee db run:all # runs all migrations for the development environmentStarting from version 1.9.0, the main issue with SQLite — write database locking — is resolved. You can tune the retry configuration parameters as needed:
## configure database write retries
config.db_max_retries = { env:, value: 3 } # set to 0 to disable, or increase if needed
config.db_retry_delay = { env:, value: 0.1 }
config.db_busy_timeout = { env:, value: 1000 } # busy timeout in milliseconds before raising an errorFor ru.Bee model create and update methods, retries are added automatically. To use retries with a Sequel dataset directly:
Rubee::DBTools.with_retry { User.dataset.insert(email: "test@ok.com", password: "123") }ru.Bee uses explicit routes. In routes.rb you can define routes for any of the main HTTP methods.
You can also include matched parameters denoted by { } in the route path, e.g. /path/to/{a_key}/somewhere.
Rubee::Router.draw do |router|
router.get '/posts', to: 'posts#index'
router.post '/posts', to: 'posts#create'
router.patch '/posts/{id}', to: 'posts#update'
router.put '/posts/{id}', to: 'posts#update'
router.delete '/posts/{id}', to: 'posts#delete'
router.head '/posts', to: 'posts#index'
router.connect '/posts', to: 'posts#index'
router.options '/posts', to: 'posts#index'
router.trace '/posts', to: 'posts#index'
endEvery route follows this structure:
route.{http_method} {path}, to: "{controller}#{action}",
model: { ...optional }, namespace: { ...optional }, react: { ...optional }One of ru.Bee's unique traits is defining models for generation directly in the routes:
Rubee::Router.draw do |router|
...
router.get "/apples", to: "apples#index",
model: {
name: "apple",
attributes: [
{ name: 'id', type: :primary },
{ name: 'colour', type: :string },
{ name: 'weight', type: :integer },
{ name: 'created', type: :datetime },
{ name: 'updated', type: :datetime },
]
}
endOther supported attribute types via Sequel:
[
{ name: 'id', type: :primary },
{ name: 'name', type: :string },
{ name: 'description', type: :text },
{ name: 'quantity', type: :integer },
{ name: 'created', type: :date },
{ name: 'modified', type: :datetime },
{ name: 'exists', type: :time },
{ name: 'active', type: :boolean },
{ name: 'hash', type: :bigint },
{ name: 'price', type: :decimal },
{ name: 'item_id', type: :foreign_key },
{ name: 'item_id_index', type: :index },
{ name: 'item_id_unique', type: :unique }
]Every attribute can carry options based on the Sequel schema definition. For example:
{ name: 'key', type: :string, options: { size: 50, fixed: true } }Gets translated to:
String :key, size: 50, fixed: trueAs long as a route has a model: key, you can use it to generate initial model files. If only path and to: are defined, only a controller and view will be generated.
rubee generate get /apples # or: rubee gen get /apples
rubee generate patch /apples/{id} # or: rubee gen patch /apples/{id}Example 1 — route without a model:
router.get "/apples", to: "apples#index"Generates:
./app/controllers/apples_controller.rb
./app/views/apples_index.erbExample 2 — route with a model name only:
router.get "/apples", to: "apples#index", model: { name: 'apple' }Generates:
./app/controllers/apples_controller.rb
./app/views/apples_index.erb
./app/models/apple.rb
./db/create_apples.rbExample 3 — route with full model attributes:
router.get "/apples", to: "apples#index",
model: {
name: 'apple',
attributes: [
{ name: 'id', type: :primary },
{ name: 'colour', type: :string },
{ name: 'weight', type: :integer },
{ name: 'created', type: :datetime },
{ name: 'updated', type: :datetime },
]
}Generates:
./app/controllers/apples_controller.rb
./app/models/apple.rb
./app/views/apples_index.erb
./db/create_apples.rbru.Bee supports modular applications — attach as many subprojects as you need. Each subproject gets its own folder, MVC setup, routes, and namespacing, while still sharing data with the main app.
- Attach a new subproject
rubee attach admin- Add routes
# admin_routes.rb
Rubee::Router.draw do |router|
router.get '/admin/cabbages', to: 'cabbages#index',
model: {
name: 'cabbage',
attributes: [
{ name: 'id', type: :primary },
{ name: 'name', type: :string },
{ name: 'created', type: :datetime },
{ name: 'updated', type: :datetime },
]
},
namespace: :admin # mandatory for namespacing support
end- Run the generate command
rubee gen get /admin/cabbages app:adminGenerates:
./admin/controllers/cabbages_controller.rb
./admin/views/cabbages_index.erb
./admin/models/cabbage.rb
./db/create_cabbages.rb- Run the migration
rubee db run:create_cabbages- Fill the controller with content
# ./admin/controllers/cabbages_controller.rb
class Admin::CabbagesController < Rubee::BaseController
def index
response_with object: Cabbage.all, type: :json
end
end- Run the server
rubee start # or rubee start_dev for developmentA view in ru.Bee is a plain HTML, ERB, or React file rendered from the controller.
layout.erb is the parent template rendered first; child templates are rendered inside it. Feel free to include custom CSS and JS files there.
# app/controllers/welcome_controller.rb
class WelcomeController < Rubee::BaseController
def show
response_with object: { message: 'Hello, world!' }
end
end<%# app/views/welcome_header.erb %>
<h1>All set up and running!</h1><%# app/views/welcome_show.erb %>
<div class="container">
<%= render_template :welcome_header %> <%# attach an ERB partial with render_template %>
<p><%= locals[:object][:message] %></p> <%# display the object passed from the controller %>
</div>React is supported out of the box as a view layer in ru.Bee.
Prerequisites: Node and NPM are required.
- After creating your project and bundling, install React dependencies:
rubee react prepare- Configure React in
config/base_configuration.rb:
Rubee::Configuration.setup(env = :development) do |config|
config.database_url = { url: 'sqlite://db/development.db', env: }
# register React as a view
config.react = { on: true, env: }
end- Start the server:
rubee start
# Default port is 7000. To change it:
rubee start --port=3000-
Open your browser and navigate to http://localhost:3000/home.
-
For development, run
rubee start_devin one terminal andrubee react watchin another. Changes apply instantly. -
In production, rebuild the React app with
rubee react build. Not needed in development when usingrubee react watch. -
Generate a React view from a route by specifying the view name:
# config/routes.rb
Rubee::Router.draw do |router|
router.get('/', to: 'welcome#show')
router.get('/api/users', to: 'user#index', react: { view_name: 'users.tsx' })
# Note: /api/users is the backend endpoint.
# To render /app/views/users.tsx, update the React routes as shown below.
end- Add logic to the generated API controller:
# app/controllers/api/user_controller.rb
class Api::UserController < Rubee::BaseController
def index
response_with object: User.all, type: :json
end
end- Register the path in React routes:
// app/views/app.tsx
<Router>
<Routes>
<Route path="/users" element={<Users />} />
<Route path="*" element={<NotFound />} />
</Routes>
</Router>- Fetch data from the backend in the component:
// app/views/users.tsx
import { useState, useEffect } from 'react';
function Users() {
const [users, setUsers] = useState([]);
useEffect(() => {
fetch('/api/users')
.then(response => response.json())
.then(data => setUsers(data));
}, []);
return (
<div>
<h1>Users</h1>
<ul>
{users.map(user => (
<li key={user.id}>id: {user.id}: {user.name}</li>
))}
</ul>
</div>
);
}By including the Hookable module, any Ruby object can be charged with hooks — logic that executes before, after, or around a specific method call.
BaseController is Hookable by default:
class ApplesController < Rubee::BaseController
before :index, :print_hello # use an instance method as a handler
after :index, -> { puts "after index" }, if: -> { true } # or use a lambda
after :index, -> { puts "after index2" }, unless: -> { false } # if/unless guards accept a method or lambda
around :index, :log
def index
response_with object: { test: "hooks" }
end
def print_hello
puts "hello!"
end
def log
puts "before log around"
res = yield
puts "after log around"
res
end
endThe server logs will show the following execution stack:
before log around
hello!
after index
after index2
after log around
127.0.0.1 - - [17/Feb/2025:11:42:14 -0500] "GET /apples HTTP/1.1" 401 - 0.0359Starting from version 1.11, hooks can also be pinned to class methods:
class AnyClass
include Rubee::Hookable
before :print_world, :print_hello, class_methods: true
class << self
def print_world
puts "world!"
end
def print_hello
puts "hello!"
end
end
endOutput:
hello!
world!Any class can be charged with validations by including the Validatable module.
ru.Bee models are validatable by default — no need to include it explicitly.
class Foo
include Rubee::Validatable
attr_accessor :name, :age
def initialize(name, age)
@name = name
@age = age
end
validate do
attribute(:name).required.type(String).condition(->{ name.length > 2 })
attribute(:age)
.required('Age is a mandatory field')
.type(Integer, error_message: 'Must be an integer!')
.condition(->{ age > 18 }, fancy_error: 'You must be at least 18 years old!')
end
endirb(main):041> Foo.new("Test", 20).valid?
=> true
irb(main):042> Foo.new("Test", 1).errors
=> {age: {fancy_error: "You must be at least 18 years old!"}}
irb(main):046> Foo.new("Joe", "wrong").valid?
=> false
irb(main):047> Foo.new("Joe", "wrong").errors
=> {age: {error_message: "Must be an integer!"}}Model example with persistence guards:
class User < Rubee::SequelObject
attr_accessor :id, :email, :password, :created
validate_after_setters # runs validation after each setter
validate_before_persist! # validates and raises an error if invalid before saving
validate do
attribute(:email).required
.condition(
->{ email.match?(/\A[\w+\-.]+@[a-z\d\-]+(\.[a-z\d\-]+)*\.[a-z]+\z/i) }, error: 'Wrong email format'
)
end
endirb(main):077> user.save
=> {email: {error: "Wrong email format"}} (Rubee::Validatable::Error)
irb(main):078> user.email = "ok@ok.com"
irb(main):080> user.save
=> trueTo apply validate_before_persist! and validate_after_setters globally, add an initializer such as init/sequel_object_preloader.rb:
Rubee::SequelObject.validate_before_persist!
Rubee::SequelObject.validate_after_settersAn optional set of useful methods can be added to base Ruby classes globally via configuration:
# Include all support methods
Rubee::Configuration.setup do |config|
config.rubee_support = { all: true }
end
# Include only methods for a specific class
Rubee::Configuration.setup do |config|
config.rubee_support = { classes: [Rubee::Support::String] }
endAvailable extensions:
# Hash — tolerates string or symbol keys interchangeably
{one: 1}[:one] # => 1
{one: 1}["one"] # => 1
# Hash — deep digging
{one: {two: 2}}.deep_dig(:two) # => 2# String — enriched with helper methods
"test".pluralize # => "tests"
"test".singularize # => "test"
"test".camelize # => "Test"
"TestMe".snakeize # => "test_me"
"test".singular? # => true
"test".plural? # => falseInclude the AuthTokenable module in your controller and authenticate any action you need.
First, initialize the User model:
rubee db run:create_usersThis creates the users table and seeds it with demo credentials — email ok@ok.com, password password. Customize /db/create_users.rb before running the migration if needed.
class UsersController < Rubee::BaseController
include Rubee::AuthTokenable
auth_methods :index # unauthenticated requests to these actions will be rejected
# GET /users/login
def edit
response_with
end
# POST /users/login
def login
if authenticate! # initializes @token_header
response_with type: :redirect, to: "/users", headers: @token_header
else
@error = "Wrong email or password"
response_with render_view: "users_edit"
end
end
# POST /users/logout
def logout
unauthenticate!
response_with type: :redirect, to: "/users/login", headers: @zeroed_token_header
end
# GET /users (restricted)
def index
response_with object: User.all, type: :json
end
endSet a JWT_KEY at startup for security:
JWT_KEY=SDJwer0wer23j rubee startTo use a custom model instead of the default User, pass arguments to authenticate! and unauthenticate!:
if authenticate! user_model: Client, login: :name, password: :digest_password
response_with type: :redirect, to: "/clients", headers: @token_header
endTo plug in OAuth 2.0 authentication, add the oauth2 gem to your Gemfile:
gem 'oauth2'Use the following as a starting point:
class UsersController < Rubee::BaseController
include Rubee::AuthTokenable
REDIRECT_URI = 'https://mysite.com/users/oauth_callback'
CLIENT_ID = ENV['GOOGLE_CLIENT_ID']
CLIENT_SECRET = ENV['GOOGLE_CLIENT_SECRET']
# GET /login
def edit
response_with
end
# POST /users/login
def login
if authenticate!
response_with(type: :redirect, to: "/sections", headers: @token_header)
else
@error = "Wrong email or password"
response_with(render_view: "users_edit")
end
end
# GET /users/oauth_login
def oauth_login
response_with(
type: :redirect,
to: auth_client.auth_code.authorize_url(
redirect_uri: REDIRECT_URI,
scope: 'email profile openid'
)
)
end
# GET /users/oauth_callback
def oauth_callback
code = params[:code]
token = auth_client.auth_code.get_token(code, redirect_uri: REDIRECT_URI)
user_info = JSON.parse(token.get('https://www.googleapis.com/oauth2/v1/userinfo?alt=json').body)
user = User.where(email: user_info['email'])&.last
raise "User with email #{user_info['email']} not found" unless user
params[:email] = user_info['email']
params[:password] = user.password
if authenticate!
response_with(type: :redirect, to: "/sections", headers: @token_header)
else
@error = "Something went wrong"
response_with(render_view: "users_edit")
end
rescue OAuth2::Error
@error = "OAuth login failed"
response_with(render_view: "users_edit")
rescue StandardError
@error = "Something went wrong"
response_with(render_view: "users_edit")
end
# POST /users/logout
def logout
unauthenticate!
response_with(type: :redirect, to: "/login", headers: @zeroed_token_header)
end
private
def auth_client
@client ||= OAuth2::Client.new(
CLIENT_ID,
CLIENT_SECRET,
site: 'https://accounts.google.com',
authorize_url: '/o/oauth2/auth',
token_url: 'https://oauth2.googleapis.com/token'
)
end
endrubee start # start the server
rubee start_dev # start the server in dev mode, restarting on file changes
rubee react prepare # install React dependencies
rubee react watch # React dev mode, use together with start_dev
rubee stop # stop the server
rubee restart # restart the serverrubee generate get /apples # generate controller, view, model, and migration if set in routes
rubee gen get /apples # shorthand aliasrubee db run:all # run all migrations
rubee db run:create_apples # run a specific migration file from /db
rubee db structure # generate a migration file for the database structurerubee console # start the interactive console
# type 'reload' inside the console to pick up the latest changesTo run any ru.Bee command in a specific environment, prefix with the env variable:
RACK_ENV=test rubee consolerubee test # run all tests
rubee test auth_tokenable_test.rb # run a specific test file
rubee test models/user_model_test.rb --line=12 # run a specific line- Add Sidekiq to your Gemfile
gem 'sidekiq'- Configure the adapter for the desired environment
# config/base_configuration.rb
Rubee::Configuration.setup(env = :development) do |config|
config.database_url = { url: "sqlite://db/development.db", env: }
config.async_adapter = { async_adapter: SidekiqAsync, env: }
end- Install dependencies
bundle install- Start Redis
redis-server- Add a Sidekiq configuration file
# config/sidekiq.yml
development:
redis: redis://localhost:6379/0
concurrency: 5
queues:
default:
low:
high:- Create a Sidekiq worker
# app/async/test_async_runner.rb
require_relative 'extensions/asyncable' unless defined? Asyncable
class TestAsyncRunner
include Rubee::Asyncable
include Sidekiq::Worker
sidekiq_options queue: :default
def perform(options)
User.create(email: options['email'], password: options['password'])
end
end- Use it in your codebase
TestAsyncRunner.new.perform_async(options: { "email" => "new@new.com", "password" => "123" })The default adapter is ThreadAsync. It is not yet recommended for production — use with caution.
- Do not define any adapter in
config/base_configuration.rb; the defaultThreadAsyncwill be used. - Create a worker and process it:
# test_async_runner.rb
class TestAsyncRunner
include Rubee::Asyncable
def perform(options)
User.create(email: options['email'], password: options['password'])
end
end
TestAsyncRunner.new.perform_async(options: { "email" => "new@new.com", "password" => "123" })Use your own logger by setting it in config/base_configuration.rb:
Rubee::Configuration.setup(env = :development) do |config|
config.database_url = { url: "sqlite://db/development.db", env: }
config.logger = { logger: MyLogger, env: }
endOr use the built-in logger with its full set of levels:
# app/controllers/welcome_controller.rb
class WelcomeController < Rubee::BaseController
around :show, ->(&target_method) do
start = Time.now
Rubee::Logger.warn(message: 'This is a warning message', method: :show, class_name: 'WelcomeController')
Rubee::Logger.error(message: 'This is an error message', class_name: 'WelcomeController')
Rubee::Logger.critical(message: 'We are on fire!')
target_method.call
Rubee::Logger.info(
message: "Execution Time: #{Time.now - start} seconds",
method: :show,
class_name: 'WelcomeController'
)
Rubee::Logger.debug(object: User.last, method: :show, class_name: 'WelcomeController')
end
def show
response_with
end
endOutput:
[2025-04-26 12:32:33] WARN [method: show][class_name: WelcomeController] This is a warning message
[2025-04-26 12:32:33] ERROR [class_name: WelcomeController] This is an error message
[2025-04-26 12:32:33] CRITICAL We are on fire!
[2025-04-26 12:32:33] INFO [method: show][class_name: WelcomeController] Execution Time: 0.000655 seconds
[2025-04-26 12:32:33] DEBUG [method: show][class_name: WelcomeController] #<User:0x000000012c5c63e0 ...>With ru.Bee 2.0.0 you can use WebSocket with ease.
- Install and start Redis
sudo apt-get install -y redis # Linux
brew install redis # macOS- Add the required gems to your Gemfile
gem 'ru.Bee'
gem 'redis'
gem 'websocket'- Add the Redis URL to your configuration, unless it defaults to
127.0.0.1:6379
# config/base_configuration.rb
Rubee::Configuration.setup(env = :development) do |config|
...
config.redis_url = { url: "redis://localhost:6378/0", env: }
end- Add a WebSocket entry route
# config/routes.rb
Rubee::Router.draw do |router|
...
router.get('/ws', to: 'users#websocket')
# On the client: const ws = new WebSocket("ws://website/ws");
end- Make the model pub/sub capable
# app/models/user.rb
class User < Rubee::BaseModel
include Rubee::PubSub::Publisher
include Rubee::PubSub::Subscriber
...
end- Enable WebSocket in your controller and implement the required methods
# app/controllers/users_controller.rb
class UsersController < Rubee::BaseController
attach_websocket! # handles WebSocket connections and routes them to publish, subscribe, unsubscribe
# Expected client params: { action: 'subscribe', channel: 'default', id: '123', subscriber: 'User' }
def subscribe
channel = params[:channel]
sender_id = params[:options][:id]
io = params[:options][:io]
User.sub(channel, sender_id, io) do |channel, args|
websocket_connections.register(channel, args[:io])
end
response_with(object: { type: 'system', channel: params[:channel], status: :subscribed }, type: :websocket)
rescue StandardError => e
response_with(object: { type: 'system', error: e.message }, type: :websocket)
end
# Expected client params: { action: 'unsubscribe', channel: 'default', id: '123', subscriber: 'User' }
def unsubscribe
channel = params[:channel]
sender_id = params[:options][:id]
io = params[:options][:io]
User.unsub(channel, sender_id, io) do |channel, args|
websocket_connections.remove(channel, args[:io])
end
response_with(object: params.merge(type: 'system', status: :unsubscribed), type: :websocket)
rescue StandardError => e
response_with(object: { type: 'system', error: e.message }, type: :websocket)
end
# Expected client params: { action: 'publish', channel: 'default', message: 'Hello', id: '123', subscriber: 'User' }
def publish
args = {}
User.pub(params[:channel], message: params[:message]) do |channel|
user = User.find(params[:options][:id])
args[:message] = params[:message]
args[:sender] = params[:options][:id]
args[:sender_name] = user.email
websocket_connections.stream(channel, args)
end
response_with(object: { type: 'system', message: params[:message], status: :published }, type: :websocket)
rescue StandardError => e
response_with(object: { type: 'system', error: e.message }, type: :websocket)
end
endFor a full chat application example, see rubee-chat.
ru.Bee ships with a built-in CLI assistant called bee. It answers questions about the framework directly in your terminal, using a local TF-IDF knowledge base built from the project documentation. Optionally, it routes answers through a local Ollama language model for richer, more conversational responses.
No external API keys or internet connection are required in the default mode.
Before using the assistant for the first time, generate the knowledge base from the README:
rubee bee generate # or: rubee bee genThis parses the documentation, computes TF-IDF vectors, and writes a bee_knowledge.json file to lib/rubee/cli/. Re-run this command any time the documentation is updated.
Start an interactive session and ask questions conversationally:
rubee bee ⬡ ⬢ ⬢ ru.Bee — domestic AI assistant
──────────────────────────────────────────────
Ask me anything about the ru.Bee framework.
Type exit to leave • rubee bee generate to retrain.
You: How do I run the server?
Type exit, quit, bye, or q to leave the session.
Pass a question directly as a command-line argument to get one answer and exit:
rubee bee how do hooks work
rubee bee what databases are supported
rubee bee how do I set up JWT authenticationIf you have Ollama installed and running locally, enable LLM mode for more detailed answers. The assistant retrieves the most relevant documentation and passes it as context to the model.
rubee bee --llm # interactive mode, default model (qwen2.5:1.5b)
rubee bee --llm=llama3.2 # interactive mode, specific model
rubee bee --llm how do hooks work # single-shot LLM answer
rubee bee --llm=qwen2.5:0.5b how do I configure WebSocket # single-shot with specific modelIf the specified model is not available locally, the assistant automatically pulls it from Ollama before answering, displaying a live download progress bar.
OLLAMA_URL=http://remote-host:11434 rubee bee --llm # use a custom Ollama endpoint
BEE_KNOWLEDGE=/path/to/custom.json rubee bee # use a custom knowledge base file
BEE_DEBUG=1 rubee bee --llm # write LLM debug output to /tmp/bee_ollama_debug.txtAfter every answer, the assistant suggests up to five related topics you might want to explore next, along with a link to the full documentation at https://rubee.dedyn.io/.
rubee bee generate # build the knowledge base from the README
rubee bee gen # alias for generate
rubee bee # start interactive mode
rubee bee <question> # single-shot answer
rubee bee --llm # interactive LLM mode (default model: qwen2.5:1.5b)
rubee bee --llm=<model> # interactive LLM mode with a specific Ollama model
rubee bee --llm <question> # single-shot LLM answer
rubee bee --llm=<model> <question> # single-shot with a specific modelIf you are interested in contributing to ru.Bee, please read the Contributing guide. Feel free to open an issue if you spot one. Have an idea or want to discuss something? Open a discussion.
Please refer to the Roadmap.
This project is released under the MIT License.

