Roda Kabk
Kabk carries the Ruby to Simorgh
roda-kabk is the official Roda plugin for Kabk — providing a Smart Dispatch routing architecture for the Simurgh Panel protocol (v1.6.0). It uses api_path defined in Kabk.register as the Single Source of Truth for all routes, giving you full control over middleware, authentication, and routing branches.
Features
- Smart Dispatch Routing (
r.kabk.route): Automatically routes incoming requests to the appropriateKabk::RestEngineresource based on each resource'sapi_path. - Global Static Assets (
r.kabk.statics): Serves pre-built assets (JS, CSS, SVGs, images, fonts, videos) under/assets/*and/simurgh-logo.svg. - Vue.js SPA Server (
r.kabk.server): Serves the single-page admin dashboard SPAindex.htmlwith embedded schema injection. - Protocol Schema Manifest (
r.kabk.schema): Exposes dynamic protocol schema JSON. - Pluggable File Uploads (
r.kabk.upload): Seamless integration with Shrine, ActiveStorage, or custom storage handlers via configurable procs. - Context Injection: Pass
contextintor.kabk.route(context: ...)to support Kabk's lifecycle hooks and audit fields based on the current user. - Optimistic Concurrency Control (OCC): Built-in support for preventing concurrent record overwrite conflicts (
updated_at/version). - Client-Side Data Export: CSV exports are natively generated on the client-side (frontend) using existing table data and active filters without backend overhead.
Installation
Add this line to your application's Gemfile:
gem 'roda-kabk'
And then execute:
bundle install
Quick Start (Smart Dispatch Architecture)
In the new decoupled architecture, Kabk no longer manages authentication internally. You handle authentication in Roda and dispatch protected CRUD requests via r.kabk.route(context: ...).
1. Setup DB and Sequel Model
Define your user and resource models with api_path as the Single Source of Truth:
require 'roda'
require 'sequel'
require 'kabk'
require 'roda/plugins/kabk'
require 'bcrypt'
DB = Sequel.sqlite
# Authentication Model
DB.create_table :admin_users do
primary_key :id
String :username, null: false, unique: true
String :password_digest, null: false
String :full_name
String :avatar_url
String :role, default: "admin"
end
class AdminUser < Sequel::Model
plugin :secure_password
end
DB.create_table :articles do
primary_key :id
String :title, null: false
String :content
DateTime :updated_at
end
class Article < Sequel::Model; end
# Register Entity with Kabk (api_path is the Single Source of Truth)
Kabk.register(name: "article", table: Article) do
title "Articles"
plural_name "articles"
api_path "/admin/api/articles"
concurrency_field "updated_at"
field :id, type: :number, form_type: :number, primary_key: true
field :title, type: :string, form_type: :text, required: true
field :content, type: :string, form_type: :textarea
field :updated_at, type: :datetime, form_type: :datetime, readonly: true
end
2. Implement Roda Application
Use Roda's routing tree with r.kabk helper methods:
class App < Roda
plugin :sessions, secret: ENV.fetch("SESSION_SECRET", "super_secret_key_that_is_long_enough_for_session")
plugin :json
plugin :kabk,
system_config: {
title: { en: "Admin Dashboard", fa: "پنل مدیریت" },
default_locale: "en",
supported_locales: ["en", "fa"],
direction: "ltr",
show_demo_credentials: false,
endpoints: {
upload: "/admin/api/uploads"
},
auth: {
strategy: "session",
sso_redirect_url: nil,
login_url: "/admin/api/auth/login",
me_url: "/admin/api/auth/me",
logout_url: "/admin/api/auth/logout",
refresh_url: "/admin/api/auth/refresh",
show_demo_credentials: false,
login_fields: [
{ name: "username", label: { en: "Username", fa: "نام کاربری" }, type: "text", required: true },
{ name: "password", label: { en: "Password", fa: "کلمه عبور" }, type: "password", required: true }
]
}
},
upload_handler: ->(file_param, req) {
{
url: "/uploads/#{file_param[:filename]}",
file_name: file_param[:filename],
size: file_param[:tempfile].size,
mime_type: file_param[:type]
}
}
route do |r|
r.kabk.statics
r.on "admin" do
r.is do
r.kabk.server
end
r.on "api" do
r.get "schema" do
r.kabk.schema
end
r.post "uploads" do
r.kabk.upload
end
r.on "auth" do
r.post "login" do
body = JSON.parse(r.body.read) rescue {}
user = AdminUser.first(username: body["username"])
if user && user.authenticate(body["password"])
session[:user_id] = user.id
{ success: true, data: { id: user.id, username: user.username, role: user.role } }
else
response.status = 401
{ success: false, error: { message: "Invalid credentials" } }
end
end
r.get "me" do
user = AdminUser[session[:user_id]]
if user
{ success: true, data: { id: user.id, username: user.username, role: user.role } }
else
response.status = 401
{ success: false, error: { message: "Unauthorized" } }
end
end
r.post "logout" do
session.clear
{ success: true, message: "Logged out" }
end
end
current_user = AdminUser[session[:user_id]]
unless current_user
response.status = 401
r.halt({ success: false, error: { message: "Unauthorized" } }.to_json)
end
r.kabk.route(context: { current_user: current_user })
end
end
end
end
Configuration Reference
The plugin :kabk method accepts the following options:
| Option | Type | Default | Description |
|---|---|---|---|
system_config |
Hash / NilClass |
nil |
Custom system branding, custom fonts, and localization settings. |
upload_handler |
Proc / Callable |
nil |
Custom file upload handler block/callable. Processes uploaded file params. |
Request Methods (r.kabk.*)
| Method | Description |
|---|---|
r.kabk.statics |
Serves static assets (JS, CSS, SVGs, images, fonts, videos) from /assets/* and simurgh-logo.svg. |
r.kabk.server |
Serves index.html (with memoized schema injection) for the Vue.js SPA dashboard. |
r.kabk.schema |
Returns the dynamic protocol schema JSON manifest. |
r.kabk.upload |
Handles multipart file uploads using upload_handler or fallback. |
r.kabk.route(context: {}) |
Smart Dispatcher: matches the request path against registered api_paths and executes CRUD actions (list, get, create, update, delete) on Kabk::RestEngine. |
Context Injection & Hooks
By passing context: { current_user: ... } to r.kabk.route, Kabk's RestEngine propagates this context to lifecycle hooks and audit fields. For example, you can write entity hooks in Kabk core that verify if a user has permission to delete a record or automatically assign the created_by field based on context[:current_user].
File Upload Integration (Shrine Example)
You can connect Shrine directly using the upload_handler option:
plugin :kabk,
upload_handler: ->(file_param, req) {
# file_param contains Rack tempfile params: { tempfile: #<File>, filename: "...", type: "..." }
uploader = ImageUploader.upload(file_param[:tempfile], :store)
{
url: uploader.url,
file_name: file_param[:filename],
size: file_param[:tempfile].size,
mime_type: file_param[:type]
}
}
If no upload_handler is provided and no file is attached, r.kabk.upload returns an HTTP 422 Unprocessable Entity error.
Testing
Run the test suite using rspec:
bundle exec rspec
License
The gem is available as open source under the terms of the MIT License.