# Rage Framework Documentation > Comprehensive guide to building applications with the Ruby Rage framework ## Api ### API Reference Complete technical documentation for all Rage framework classes and modules. *** #### Core Components ##### [Controller](https://api.rage-rb.dev/RageController/API) Base controller functionality for handling HTTP requests and responses. ##### [Router](https://api.rage-rb.dev/Rage/Router/DSL/Handler) Define and manage application routes. ##### [Configuration](https://api.rage-rb.dev/Rage/Configuration) Configure your Rage application settings and environment. *** #### HTTP Handling ##### [Request](https://api.rage-rb.dev/Rage/Request) Access and manipulate incoming HTTP request data. ##### [Response](https://api.rage-rb.dev/Rage/Response) Build and send HTTP responses to clients. ##### [Cookies](https://api.rage-rb.dev/Rage/Cookies) Read and write HTTP cookies. ##### [Session](https://api.rage-rb.dev/Rage/Session) Manage user session data across requests. ##### [UploadedFile](https://api.rage-rb.dev/Rage/UploadedFile) Handle file uploads from multipart form data. ##### [SSE::Message](https://api.rage-rb.dev/Rage/SSE/Message) Build Server-Sent Events messages. ##### [SSE::ConnectionProxy](https://api.rage-rb.dev/Rage/SSE/ConnectionProxy) Low-level interface for SSE connections. *** #### Middleware ##### [CORS Middleware](https://api.rage-rb.dev/Rage/Cors) Enable Cross-Origin Resource Sharing for your API. ##### [RequestID Middleware](https://api.rage-rb.dev/Rage/RequestId) Mark requests with unique identifiers for logging and debugging. ##### [EnqueueMiddlewareInterface](https://api.rage-rb.dev/EnqueueMiddlewareInterface) Intercept and modify deferred task enqueueing operations. ##### [PerformMiddlewareInterface](https://api.rage-rb.dev/PerformMiddlewareInterface) Intercept and modify deferred task execution. *** #### Telemetry ##### [Handler](https://api.rage-rb.dev/Rage/Telemetry/Handler) Monitor and respond to specific span executions in your application. ##### [Spans](https://api.rage-rb.dev/Rage/Telemetry/Spans) Telemetry span definitions for tracing framework operations and events. ##### [SpanResult](https://api.rage-rb.dev/Rage/Telemetry/SpanResult) Access the outcome and status of telemetry span execution. *** #### Async & Concurrency ##### [Fiber](https://api.rage-rb.dev/Fiber) Work with Ruby fibers for lightweight concurrency. ##### [Deferred](https://api.rage-rb.dev/Rage/Deferred) Handle deferred execution and asynchronous operations. ##### [Daemon](https://api.rage-rb.dev/Rage/Daemon) Build and manage long-lived background processes. *** #### Events ##### [Events](https://api.rage-rb.dev/Rage/Events) Build event-driven applications with the Rage event system. ##### [Subscriber](https://api.rage-rb.dev/Rage/Events/Subscriber/ClassMethods) Subscribe to and handle application events. *** #### WebSockets ##### [Channel](https://api.rage-rb.dev/Rage/Cable/Channel) Create WebSocket channels for real-time communication. ##### [Connection](https://api.rage-rb.dev/Rage/Cable/Connection) Manage WebSocket connection lifecycle and authentication. *** #### Utilities ##### [Logger](https://api.rage-rb.dev/Rage/Logger) Log application messages with configurable levels and formats. --- ## Blog ### Blog #### [Rage in the Wild](https://rage-rb.dev/blog/2026/07/04/rage-in-the-wild.md) July 4, 2026 · One min read [![The Rage Team](/img/authors/rage.svg)](mailto:developers@rage-rb.dev) [The Rage Team](mailto:developers@rage-rb.dev) [](https://github.com/rage-rb "GitHub") Wrote an article exploring real-world patterns from popular Ruby open-source codebases and showing how they could be modelled using Rage. [**Read more**](https://rage-rb.dev/blog/2026/07/04/rage-in-the-wild.md) --- ### Version 0.1 Release September 15, 2023 · One min read [![The Rage Team](/img/authors/rage.svg)](mailto:developers@rage-rb.dev) [The Rage Team](mailto:developers@rage-rb.dev) [](https://github.com/rage-rb "GitHub") Today marks the first official release of Rage. This initial version includes the foundational pieces: * Basic controller API for handling HTTP requests * Router implementation with URL matching * Fiber scheduler for non-blocking I/O operations It's a starting point - plenty rough around the edges, but the core architecture is in place. Looking forward to building on this foundation. --- ### Version 0.7 Release January 9, 2024 · One min read [![The Rage Team](/img/authors/rage.svg)](mailto:developers@rage-rb.dev) [The Rage Team](mailto:developers@rage-rb.dev) [](https://github.com/rage-rb "GitHub") Version 0.7 is a significant milestone - all the features planned for the initial release are now implemented. The framework has the essential pieces in place: * Complete routing and controller functionality * Middleware support * Request/response handling * Error handling and logging Now comes the harder part: production testing and stabilization. This means finding edge cases, fixing bugs, and ensuring everything works reliably under real-world conditions. It's going to be a long journey, but this marks the transition from building to hardening. --- ### WebSocket Support August 6, 2024 · One min read [![The Rage Team](/img/authors/rage.svg)](mailto:developers@rage-rb.dev) [The Rage Team](mailto:developers@rage-rb.dev) [](https://github.com/rage-rb "GitHub") Version 1.8.0 adds WebSocket support through an Action Cable-compatible API. If you're familiar with Action Cable, the interface will feel immediately familiar - same channels, same broadcasting patterns. The difference is in the implementation. By leveraging Rage's fiber-based concurrency model and non-blocking I/O, we're seeing significantly better performance compared to the standard Action Cable setup. Same developer experience, but faster and more efficient under load. --- ### OpenAPI Documentation December 18, 2024 · One min read [![The Rage Team](/img/authors/rage.svg)](mailto:developers@rage-rb.dev) [The Rage Team](mailto:developers@rage-rb.dev) [](https://github.com/rage-rb "GitHub") Version 1.11.0 brings `Rage::OpenAPI` for API documentation. The goal was to make documenting APIs less painful - you document your APIs using YARD-style tags, and it generates OpenAPI 3.0 specs automatically. What makes it nice: * Automatic generation based on route definitions * Swagger UI Integration * No need to maintain separate documentation files It integrates directly with your controllers, so the documentation stays close to the code it describes. --- ### TechEmpower Benchmarks March 11, 2025 · One min read [![The Rage Team](/img/authors/rage.svg)](mailto:developers@rage-rb.dev) [The Rage Team](mailto:developers@rage-rb.dev) [](https://github.com/rage-rb "GitHub") TechEmpower Round 23 results are in. Rage shows solid performance improvements over other Ruby frameworks: * 81-219% faster than Rails across database tests * 31-100% faster than Sinatra in the same benchmarks These numbers reflect what the architecture was designed for - efficient handling of I/O-bound operations through non-blocking I/O and fiber-based concurrency. Database operations are typically the bottleneck in web apps, so this is where the differences show up most clearly. [Web Framework Benchmarks](https://www.techempower.com/benchmarks/#section=data-r23\&test=json) --- ### Rage REST API Tutorial April 13, 2025 · One min read [![The Rage Team](/img/authors/rage.svg)](mailto:developers@rage-rb.dev) [The Rage Team](mailto:developers@rage-rb.dev) [](https://github.com/rage-rb "GitHub") We collaborated with Zuplo on a tutorial for building REST APIs with Rage. The guide walks through creating a full-featured API application from scratch: * Setting up a new Rage project * Implementing CRUD operations * Integrating with Active Record * Adding WebSocket support * Generating OpenAPI docs It's a practical, hands-on guide that shows how the pieces fit together in a real application. If you're new to Rage or want to see how to structure a production app, this is a good starting point. [Building High Performance Ruby REST APIs with Rage](https://zuplo.com/learning-center/ruby-rage-rest-api-tutorial) --- ### Cloudflare Support July 17, 2025 · One min read [![The Rage Team](/img/authors/rage.svg)](mailto:developers@rage-rb.dev) [The Rage Team](mailto:developers@rage-rb.dev) [](https://github.com/rage-rb "GitHub") Rage was accepted into Cloudflare's Project Alexandria today. Cloudflare supporting Rage signals that they see value in pushing the Ruby ecosystem forward and advancing Ruby beyond its traditional use cases. That's exciting for the ecosystem as a whole. --- ### Background Tasks August 20, 2025 · One min read [![The Rage Team](/img/authors/rage.svg)](mailto:developers@rage-rb.dev) [The Rage Team](mailto:developers@rage-rb.dev) [](https://github.com/rage-rb "GitHub") Version 1.17.0 introduces `Rage::Deferred` for background task execution. The idea is straightforward - you have operations that don't need to block the request-response cycle (sending emails, processing images, updating caches), so push them to the background. `Rage::Deferred` integrates with Rage's concurrency model: * Tasks run in separate fibers * Non-blocking I/O operations stay efficient * Failed tasks can be retried It's not trying to be a full job queue system like Sidekiq. It's focused on the common case: deferring work within the same process to keep request handlers fast and responsive. --- ### Impractical Ruby Optimisations September 30, 2025 · One min read [![The Rage Team](/img/authors/rage.svg)](mailto:developers@rage-rb.dev) [The Rage Team](mailto:developers@rage-rb.dev) [](https://github.com/rage-rb "GitHub") Wrote an article exploring some unconventional performance optimizations in Ruby. It uses building an event bus for Rage as a case study to dig into: * Profiling * Implicit and explicit string conversions * Unexpected performance bottlenecks using hashes * When micro-optimizations actually matter (and when they don't) The title says "impractical" because most of these techniques aren't worth applying in typical application code. But they're interesting to understand, and they matter when you're building framework-level code where small improvements compound. [Impractical Ruby Optimisations](https://dev.to/roman_samoilov_152a8ec4ca/impractical-ruby-optimisations-2f4g) --- ### Observability Features January 23, 2026 · 2 min read [![The Rage Team](/img/authors/rage.svg)](mailto:developers@rage-rb.dev) [The Rage Team](mailto:developers@rage-rb.dev) [](https://github.com/rage-rb "GitHub") Versions 1.19.0 and 1.20.0 brought a set of observability features designed to make debugging and monitoring Rage applications easier. The focus was on giving you control over your logs and visibility into what's happening inside the framework. #### Global Log Context Version 1.19.0 introduced global log context through `config.log_tags` and `config.log_context`. You can now add custom information to every log entry your application produces - things like trace IDs, span IDs, environment names, or version numbers. The configuration accepts both static values and callables for dynamic data: ```ruby Rage.configure do config.log_context << proc do { trace_id: MyObservabilitySDK.trace_id, span_id: MyObservabilitySDK.span_id } end end ``` This means your observability platform gets consistent, structured metadata across all log entries without manually adding it to each log call. #### External Loggers Also in 1.19.0, Rage added support for piping raw structured logging data directly to external observability tools. Instead of serializing logs to text and then parsing them back, you can connect Rage's logger straight to platforms like Datadog, Sentry, or your custom solution. The `config.logger` option now accepts a callable that receives structured data for each log entry: ```ruby Rage.configure do config.logger = MyExternalLogger end ``` This gives you full control over how logging data reaches your observability platform and eliminates unnecessary serialization overhead. #### Rage::Telemetry Version 1.20.0 introduced a built-in telemetry system. It's based on spans - instrumentation points that wrap framework operations like controller actions, cable actions, and deferred tasks. You create handlers to observe these spans and integrate with your monitoring tools. Track operation durations, record failures, send metrics to external platforms - whatever you need to understand your application's behavior in production. The telemetry system is designed to be passive, so buggy observability code won't break your application. *** These three features work together to give you the tools needed to understand what's happening in production. Whether you're integrating with external platforms or building custom monitoring solutions, you now have the hooks to do it without fighting the framework. --- ### The Operational Monolith February 20, 2026 · One min read [![The Rage Team](/img/authors/rage.svg)](mailto:developers@rage-rb.dev) [The Rage Team](mailto:developers@rage-rb.dev) [](https://github.com/rage-rb "GitHub") Wrote an article exploring what Rails might look like if it was designed around modern operational constraints from day one. The core idea is an "operational monolith": keep the deployment and developer ergonomics of a monolith while making observability, reliability, and runtime behavior first-class concerns. If you are interested in framework design tradeoffs between simplicity and operational control, this article goes through the approach in detail. [If Rails Was Designed Today: The Operational Monolith](https://dev.to/roman_samoilov_152a8ec4ca/if-rails-was-designed-today-the-operational-monolith-44lh) --- ### SSE Benchmark April 9, 2026 · One min read [![The Rage Team](/img/authors/rage.svg)](mailto:developers@rage-rb.dev) [The Rage Team](mailto:developers@rage-rb.dev) [](https://github.com/rage-rb "GitHub") Ran a benchmark to test SSE performance under sustained load. Two workers on a €12/month CCX13 Hetzner server handling 5K RPS to a SQLite-backed endpoint plus 5K concurrent SSE streams. The setup: * 5K requests per second to an endpoint querying SQLite * 5K simultaneous SSE streams (5-10 seconds each, one message per second) * Streams replaced immediately on close to maintain constant 5K connections * Two Rage processes * 5 minute test duration Results: * **120ms** p95 latency * **7.75s** average stream duration * **1,735,229** requests processed * **1,486,432** SSE messages sent * **0** errors ![SSE Benchmark Results](/assets/images/sse-benchmark-results-0449f474e4ca6dd35f5fd94d908a20a9.webp) The CCX13 server handles ~11K RPS to the SQLite endpoint without any streams. Adding 5K concurrent streams cuts throughput roughly in half instead of collapsing it - the fiber model handles I/O multiplexing the way it's supposed to, and scaling looks linear. [SSE Benchmark Repository](https://github.com/rage-rb/sse-benchmark) --- ### Production Results May 11, 2026 · 2 min read [![The Rage Team](/img/authors/rage.svg)](mailto:developers@rage-rb.dev) [The Rage Team](mailto:developers@rage-rb.dev) [](https://github.com/rage-rb "GitHub") Two production Rails apps were migrated to Rage and put through 30-second spike tests. Both are I/O-bound workloads - the kind where fiber-based concurrency should help. #### Document Management Service Three read-heavy endpoints from a production Rails app: file metadata lookups, folder listings with permission-aware queries, and company-wide document libraries. Each involves multiple Postgres round-trips, authorization checks, and JSON serialization of nested objects. No CPU-heavy work - just database waits and Ruby-side object hydration. | Stack | p95 latency | Throughput | Errors | | ----- | ----------- | ---------- | ------ | | Rage | 842ms | 73 RPS | 0% | | Rails | 5s | 63 RPS | 0% | **6x lower tail latency** with 16% higher throughput. #### PDF Generation Service A small standalone app that converts HTML to PDF. Two endpoints: a write-only enqueue endpoint (~98% of traffic) that inserts a Postgres row and queues a background job, and a long-poll fetch endpoint (~2%) that sleeps in 0.5-second increments while waiting for the worker to produce the file. This is close to a best-case for fiber-based concurrency - one endpoint is bounded entirely by database latency, the other spends most of its time in `sleep` and small I/O calls. | Stack | p95 latency | Throughput | Errors | | ----- | ----------- | ---------- | ------ | | Rage | 1.77s | 71 RPS | 0% | | Rails | 17s | 49 RPS | 0.28% | **10x lower tail latency**, 45% higher throughput, and no errors. #### Takeaway The important detail is the workload shape. These were real production endpoints, not hand-picked micro-benchmarks, but they were also a very good fit for Rage: lots of waiting on I/O, very little time spent doing CPU-bound Ruby work. That is where Rage is expected to win. Under a thread-per-request server, every in-flight request holds an entire worker thread while waiting on the database. With Rage's fiber-based model, that wait time is multiplexed - the runtime can handle other requests while one waits on I/O. --- ### Rage in the Wild July 4, 2026 · One min read [![The Rage Team](/img/authors/rage.svg)](mailto:developers@rage-rb.dev) [The Rage Team](mailto:developers@rage-rb.dev) [](https://github.com/rage-rb "GitHub") Wrote an article exploring real-world patterns from popular Ruby open-source codebases and showing how they could be modelled using Rage. The article looks at three projects - Discourse, Mastodon, and GitLab - and a pattern from each: * **Request fan-out** (Discourse): Sequential HTTP requests become concurrent with `Fiber.schedule` and `Fiber.await` * **Streaming** (Mastodon): A separate ~1400-line Node.js streaming server becomes a single controller action with `Rage::SSE` * **Domain events** (GitLab): A custom event system with a separate subscription registry becomes inline subscriber declarations with `Rage::Events` The common thread: what would normally require extra complexity, infrastructure, or indirection becomes a few lines of application code when the framework handles the machinery. [Applying some Rage to Discourse, Mastodon, and GitLab](https://www.reddit.com/r/ruby/s/JQgds8SrD0) --- ### Blog #### [OpenAPI Documentation](https://rage-rb.dev/blog/2024/12/18/openapi.md) December 18, 2024 · One min read [![The Rage Team](/img/authors/rage.svg)](mailto:developers@rage-rb.dev) [The Rage Team](mailto:developers@rage-rb.dev) [](https://github.com/rage-rb "GitHub") Version 1.11.0 brings `Rage::OpenAPI` for API documentation. The goal was to make documenting APIs less painful - you document your APIs using YARD-style tags, and it generates OpenAPI 3.0 specs automatically. [**Read more**](https://rage-rb.dev/blog/2024/12/18/openapi.md) --- ## Docs ### Common Patterns This guide covers common patterns and best practices for building applications with Rage. #### Concurrent Processing ##### Problem Consider the following controller: ```ruby class UsersController < RageController::API def show user = Net::HTTP.get(URI("http://users.service/users/#{params[:id]}")) bookings = Net::HTTP.get(URI("http://bookings.service/bookings?user_id=#{params[:id]}")) render json: { user: user, bookings: bookings } end end ``` This code fires two consecutive HTTP requests. If each request takes 1 second, the total execution time will be 2 seconds. ##### Solution Rage allows you to significantly reduce execution time by firing requests concurrently using fibers: 1. Wrap each request in a separate fiber using `Fiber.schedule` 2. Pass the newly created fibers into `Fiber.await` ```ruby class UsersController < RageController::API def show user, bookings = Fiber.await([ Fiber.schedule { Net::HTTP.get(URI("http://users.service/users/#{params[:id]}")) }, Fiber.schedule { Net::HTTP.get(URI("http://bookings.service/bookings?user_id=#{params[:id]}")) } ]) render json: { user: user, bookings: bookings } end end ``` With this change, both requests execute concurrently. If each request takes 1 second, the total execution time is still 1 second. info Many developers think of fibers as "lightweight threads" that require fiber pools, similar to thread pools for threads. Instead, treat fibers as regular Ruby objects. Just as we create arrays on demand without using an "array pool", you can create fibers freely and let Ruby and the garbage collector manage them. #### Environment-Specific Code Use `Rage.env` to write environment-aware code. This is particularly useful for enabling development features or test helpers that shouldn't run in production. ##### Example: Test Authentication In this example, we allow test token authentication in non-production environments: ```ruby class ApplicationController < RageController::API before_action :authenticate_user private def authenticate_user is_user_authenticated = verify_user_token unless Rage.env.production? is_user_authenticated ||= request.headers["Test-Token"] == ENV["TEST_TOKEN"] end head :forbidden unless is_user_authenticated end end ``` `Rage.env` dynamically detects the current environment. If you start the server with `rage s -e preprod`, then `Rage.env.preprod?` will return `true`. #### Delaying Initialization ##### Problem Sometimes you need to reference application-specific constants (like models) in your initializers located in `config/initializers`. However, initializers run before the application code loads, so these constants aren't yet available. ##### Solution Use the `Rage.config.after_initialize` method to schedule code to run after the application loads. In this example, we ensure an admin user always exists. Since `User` is an application-level constant, we use `after_initialize` to delay execution until `User` is available: config/initializers/admin\_user.rb ```ruby Rage.config.after_initialize do User.find_or_create_by!(username: "admin", password: "admin") end ``` ##### When to Use This Use `after_initialize` when you need to: * Reference application models or controllers in initializers * Run setup code that depends on the full application being loaded * Configure gems that need access to your application's constants #### File Server ##### Problem You need to serve static files to clients, such as configuration files for the frontend, custom documentation pages, or other assets. ##### Solution Enable static file serving in your Rage configuration: ```ruby Rage.configure do config.public_file_server.enabled = true end ``` Once enabled, Rage will serve files from the `public` folder. Files are served at the root path based on their location in the directory. ##### Example If you have the following file structure: ```text public/ ├── config.json └── docs/ └── index.html ``` These files will be accessible at: * `/config.json` * `/docs/index.html` #### Error Reporting `Rage::Errors` provides a centralized interface for error reporting across all Rage components. Controllers, deferred tasks, event subscribers, Cable apps, and SSE streams all forward caught exceptions through this interface, eliminating the need to add manual error handling in each location. ##### Configuration Add error reporters to receive exceptions caught by the framework: ```ruby Rage.configure do config.error_reporters << MyErrorReporter.new end ``` Each reporter must respond to `call` and accept an exception: ```ruby class MyErrorReporter def call(exception) Sentry.capture_exception(exception) end end ``` You can configure multiple reporters, and all will be invoked when an exception occurs. ##### Manual Reporting In rare cases where you need to report an error explicitly: ```ruby Rage::Errors.report(exception) ``` ##### Best Practices Errors generally fall into two categories: **Expected errors** represent flows that can naturally occur in your application. Handle these explicitly and log them: ```ruby begin authenticate_user!(user) rescue UserNotAuthenticatedError Rage.logger.error "invalid password when signing in", user_id: user.id end ``` **Unexpected errors** are conditions that shouldn't occur during normal operation. Let the framework catch these and report them through `Rage::Errors`: ```ruby # Let database connection errors bubble up User.create!(user_params) ``` This means application code should rarely need to call `Rage::Errors.report` directly - the framework handles unexpected errors automatically. #### Custom Renderers Rage allows you to define custom renderers to integrate your preferred templating libraries. This is useful for rendering HTML with libraries like Phlex, Slim, or any other templating system. ##### Configuration Define a custom renderer in your Rage configuration using `config.renderer`. The block executes in the controller context, giving you access to `params`, `headers`, `cookies`, and other controller methods: config/application.rb ```ruby Rage.configure do config.renderer(:phlex) do |component, **props| headers["content-type"] = "text/html" component.new(**props).call end end ``` The renderer receives: * The first argument passed to `render` (in this case, the component class) * Any additional keyword arguments as `**props` The block's return value becomes the response body. ##### Usage Once configured, use your custom renderer by name in any controller: ```ruby class GreetingsController < RageController::API def show render phlex: GreetingComponent, name: params[:name] end end ``` This calls your configured `:phlex` renderer with `GreetingComponent` as the first argument and `name:` as a prop. ##### Example: Slim Templates Here's another example using Slim templates: config/application.rb ```ruby Rage.configure do config.renderer(:slim) do |template, **locals| headers["content-type"] = "text/html" Slim::Template.new("app/views/#{template}.slim").render(self, **locals) end end ``` ```ruby render slim: "users/show", user: @user ``` #### Agent Skills Rage provides official skills for coding agents to help them understand and work with Rage applications effectively. Skills give agents context about Rage-specific patterns, APIs, and best practices. ##### Installing Skills Run the following command in your Rage project directory: ```bash rage skills install ``` This installs agent skills based on the coding agent you use (e.g., Claude Code, Cursor, etc.). ##### Updating Skills To update skills to the latest version: ```bash rage skills update ``` #### OpenTelemetry Rage has an official OpenTelemetry integration for distributed tracing and observability. Install the [opentelemetry-instrumentation-rage](https://github.com/rage-rb/opentelemetry-instrumentation) gem to enable automatic instrumentation of your Rage application. --- ### Controllers Controllers are the heart of your Rage application. Rage follows the **MVC** (Model-View-Controller) pattern, with a focus on the **C** (Controller) layer, which handles incoming requests and coordinates responses. #### Basic Controller Structure In Rage, each controller typically represents a resource in your application (like users, posts, or orders). The public methods you define in a controller class are called **actions**, which handle specific operations on that resource: app/controllers/users\_controller.rb ```ruby class UsersController < ApplicationController def create # create a user end end ``` #### Accessing Request Parameters Rage provides a `params` hash that gives you access to all request data. It automatically parses: * Query string parameters (e.g., `?email=user@example.com`) * JSON request bodies * URL-encoded form data * URL path parameters (e.g., `/users/:id`) Here's how to use it: app/controllers/users\_controller.rb ```ruby class UsersController < ApplicationController def create User.create!(email: params[:email], password: params[:password]) end end ``` #### Rendering Responses Use the `render` method to send responses back to the client. You can specify the response status, body, headers, and more: app/controllers/users\_controller.rb ```ruby class UsersController < ApplicationController def create user = User.create!(email: params[:email], password: params[:password]) render json: { id: user.id }, status: :created end end ``` info Refer to the [API Documentation](https://api.rage-rb.dev/RageController/API) for a complete set of methods available inside controllers. #### Callbacks Callbacks let you run code before or after an action executes. They're perfect for reducing duplication and implementing cross-cutting concerns like authentication and authorization. ##### Using `before_action` The most commonly used callback is `before_action`, which runs before your controller action is executed. Here's an example that eliminates repeated code: app/controllers/user\_statuses\_controller.rb ```ruby class UserStatusesController < ApplicationController before_action do @user = User.find_by(params[:id]) end def create @user.activate! end def destroy @user.deactivate! end end ``` ##### Halting Request Processing If you call `render` or `head` inside a `before_action`, Rage will stop processing and skip the controller action entirely. This is particularly useful for authentication and authorization: app/controllers/user\_statuses\_controller.rb ```ruby class UserStatusesController < ApplicationController before_action do @user = User.find_by(params[:id]) head :bad_request unless @user end def create @user.activate! end def destroy @user.deactivate! end end ``` In this example, if the user isn't found, Rage returns a 400 Bad Request response and never calls the `create` or `destroy` actions. #### Error Handling Rage provides `rescue_from` to catch and handle exceptions in a centralized way. This is especially useful for: * Logging errors consistently * Returning standardized error responses * Handling specific exception types differently Define error handlers in your `ApplicationController` to apply them across all controllers: app/controllers/application\_controller.rb ```ruby class ApplicationController < RageController::API rescue_from StandardError do |error| render json: { error: error.message }, status: :internal_server_error end end ``` When an exception occurs in any action, Rage will execute the matching `rescue_from` block instead of letting the error bubble up. #### RESTful Design While you can name controller actions anything you want, following REST conventions leads to more maintainable and predictable APIs. The five standard RESTful actions are: | Action | Purpose | HTTP Method | | --------- | --------------------------- | ----------- | | `index` | List all resources | GET | | `show` | Display a single resource | GET | | `create` | Create a new resource | POST | | `update` | Update an existing resource | PUT/PATCH | | `destroy` | Delete a resource | DELETE | ##### When to Create a New Controller If you find yourself adding an action with a non-standard name, it's often a sign that you should create a new controller representing a different resource. **Instead of this:** app/controllers/users\_controller.rb ```ruby class UsersController < ApplicationController def stats user = User.find(params[:id]) render json: { signed_up_at: user.signed_up_at, subscription_status: user.subscription_status } end end ``` **Do this:** app/controllers/user\_stats\_controller.rb ```ruby class UserStatsController < ApplicationController def index user = User.find(params[:user_id]) render json: { signed_up_at: user.signed_up_at, subscription_status: user.subscription_status } end end ``` This approach keeps your API organized and makes it easier to understand at a glance. --- ### Background Processing `Rage::Deferred` is Rage's built-in background job queue that runs in the same process as your web server. It offloads long-running tasks from request handling, allowing you to respond to clients faster. #### How It Works ##### Fiber-Based Execution Tasks execute in fibers using Rage's fiber scheduler, making `Rage::Deferred` far more efficient than traditional thread-based background job processors. ##### Write-Ahead Log (WAL) All tasks are persisted to a write-ahead log, providing durability and reliability: * **Crash recovery**: Tasks survive server restarts and crashes * **Graceful shutdown**: On restart, Rage waits up to 15 seconds for in-progress tasks to complete * **No external dependencies**: The default disk-based log works out of the box #### When to Use `Rage::Deferred` `Rage::Deferred` excels at I/O-heavy background tasks: ✅ **Great for:** * API calls to slow or unreliable services * Sending email notifications * Data synchronization with external services * Generating reports * Streaming updates to upstream systems ❌ **Not ideal for:** * CPU-intensive computations (use a separate background job processor) * Scheduling large numbers (10,000+) of tasks far in the future (increases memory usage) #### Basic Usage ##### Defining Tasks Create a task by including the `Rage::Deferred::Task` module and implementing a `perform` method: ```ruby class SayHello include Rage::Deferred::Task def perform(name:) sleep 5 Rage.logger.info "Hello, #{name}!" end end ``` ##### Enqueuing Tasks Call `enqueue` on your task class to schedule execution: ```ruby SayHello.enqueue(name: "World") ``` This method: 1. Serializes the task and writes it to the write-ahead log 2. Returns immediately 3. Executes the task when Rage has available capacity Logs produced within tasks are automatically tagged with the originating request ID and task name. If a task fails, `Rage::Deferred` automatically retries it up to 20 times with increasing delays between attempts. The first few retries happen within seconds to minutes, while later retries are spaced hours apart, with the final retry occurring approximately 44 hours after the previous attempt. ##### Customizing Retries To change the maximum number of retries for a task: ```ruby class ProcessPayment include Rage::Deferred::Task max_retries 5 end ``` For more control, override the `retry_interval` method to implement custom logic. Return an `Integer` to set the retry interval in seconds, `super` to use the default backoff, or `false`/`nil` to abort retries: ```ruby class ProcessPayment include Rage::Deferred::Task def self.retry_interval(exception, attempt:) case exception when TemporaryNetworkError 10 # Retry in 10 seconds when InvalidDataError false # Do not retry else super # Default backoff strategy end end def perform(payment_id) # ... end end ``` ##### Delayed Execution Schedule tasks to run in the future: ```ruby # Run after 10 seconds SayHello.enqueue(name: "World", delay: 10) # Run at a specific time SayHello.enqueue(name: "World", delay_until: Time.now + 10) ``` ##### Scheduled Tasks Define recurring tasks that run on a fixed schedule: ```ruby Rage.configure do config.deferred.schedule do every 5.minutes, task: ClearCache end end ``` ##### Wrapping Existing Classes Use `Rage::Deferred.wrap` to enqueue any object without including the module: ```ruby class EmailService def self.send_welcome(email:) # Send email... end end # Synchronous execution EmailService.send_welcome(email: "user@example.com") # Background execution Rage::Deferred.wrap(EmailService).send_welcome(email: "user@example.com") ``` #### Backpressure Control Under normal conditions, Rage automatically balances between handling requests and processing background tasks. However, if each request creates many deferred tasks, the queue can grow faster than it's processed. To prevent this, `Rage::Deferred` can be configured to apply backpressure when the queue exceeds a specific number of tasks. ##### Enabling Backpressure Configure Rage to block task enqueuing when the queue gets too large: ```ruby Rage.configure do config.deferred.backpressure = true end ``` With backpressure enabled: 1. `enqueue` blocks when the queue is full (up to 2 seconds by default) 2. If the queue doesn't reduce enough within 2 seconds, `Rage::Deferred::PushTimeout` is raised ```ruby def create SayHello.enqueue(name: "World") rescue Rage::Deferred::PushTimeout head 503 end ``` See the [configuration documentation](https://api.rage-rb.dev/Rage/Configuration/Deferred) for available options. #### Middleware `Rage::Deferred` supports middleware that intercepts task behavior at two points: * **Enqueue Middleware**: Runs when tasks are added to the queue * **Perform Middleware**: Runs when tasks execute Use middleware to add context, validate arguments, track metrics, or conditionally prevent task execution. info Refer to the API Documentation for complete details and examples: * [Enqueue Middleware](https://api.rage-rb.dev/EnqueueMiddlewareInterface) * [Perform Middleware](https://api.rage-rb.dev/PerformMiddlewareInterface) #### Benefits of In-Process Execution Running background tasks in the same process provides several advantages: ##### Simplified Operations * **No separate processes**: No need to manage, monitor, or scale separate background workers * **Unified monitoring**: Background tasks are part of the request workflow * **Zero setup**: No external database required with the disk-based WAL ##### Easy Scaling If response times increase, it means Rage is spending more time on background tasks. The solution is simple: add more servers. The same horizontal scaling that improves request handling automatically improves background task processing. #### Benchmarks **Test Environment:** * AWS EC2 `m5.large` instance * Ruby 3.4.5 with YJIT enabled ##### Benchmark 1: Processing 500,000 Tasks Source Code ```ruby # app/tasks/load_task.rb class LoadTask include Rage::Deferred::Task @@count = 0 def perform @@count += 1 if @@count == 500_000 Rage.logger.info "Tasks completed at #{Time.now.to_f}" end end end # app/controller/application_controller.rb require "benchmark" class ApplicationController def index time_to_enqueue = Benchmark.realtime { enqueue_tasks } * 1_000 Rage.logger.info "Time to enqueue: #{time_to_enqueue}" Rage.logger.info "Enqueued tasks at #{Time.now.to_f}" head :ok end def enqueue_tasks 500_000.times do LoadTask.enqueue end end end ``` **Results:** * **Time to enqueue**: 3.4 seconds * **Time to process**: 6.25 seconds * **Throughput**: 80,000 tasks/second ##### Benchmark 2: Scheduling 10,000 Delayed Tasks Source Code ```ruby # app/tasks/load_task.rb class LoadTask include Rage::Deferred::Task end # app/controller/application_controller.rb require "benchmark" class ApplicationController def index time_to_enqueue = Benchmark.realtime { enqueue_tasks } * 1_000 Rage.logger.info "Time to enqueue: #{time_to_enqueue}" head :ok end def enqueue_tasks 10_000.times do LoadTask.enqueue(delay_until: Time.now + 3600) end end end ``` Tasks scheduled one hour in the future to measure memory and storage overhead. **Results:** * **Time to enqueue**: 526ms * **RAM usage**: 67 MB * **WAL file size**: 938 KB --- ### Event System Most Ruby applications describe **how** things happen. Rage lets you describe **what** happened - and the system responds. In a typical application, behavior is hidden inside method calls: ```ruby def create_order(params) order = Order.create!(params) send_confirmation_email(order) update_inventory(order) notify_analytics(order) create_shipment(order) end ``` Each new requirement adds another call. Over time, behavior becomes tangled, implicit, and hard to change. With Rage, you can take a different approach. #### Events as First-Class Behavior With `Rage::Events`, when something meaningful happens, you publish an event: ```ruby Rage::Events.publish(OrderCreated.new(order:)) ``` You don't decide who reacts. You don't wire dependencies. You don't modify existing code. You simply state a fact: *"An order was created".* Other parts of the system can react to this event: ```ruby class SendConfirmationEmail include Rage::Events::Subscriber subscribe_to OrderCreated def call(event) # send the confirmation email end end ``` Later, you can add more reactions - analytics, inventory updates, auditing, integrations. None of them require touching the original code. The system grows outward, not inward. info `Rage::Events` is designed for decoupling components **within the same application process**. For distributed systems with inter-service communication, consider dedicated messaging systems like RabbitMQ, Kafka, or AWS SNS. ##### Events Are Objects, Not Hashes Events in Rage are instances, not hashes: ```ruby OrderCreated = Data.define(:order) ``` This means: * Events are **typed** * Events are **inheritable** * Events are **testable** You can specialize behavior using inheritance: ```ruby class PriorityOrderCreated < OrderCreated end ``` Handlers can react to a specific event or an entire class of events. This is object-oriented design, applied to system behavior. #### How It Works Using `Rage::Events` involves three steps: ##### 1. Define an Event An event represents a significant occurrence within your business domain. You can use virtually any Ruby class as an event, but the best practice is to use Ruby's standard `Data` class. Instances of `Data` are immutable and support explicit initialization, making them ideal for defining events: ```ruby OrderCreated = Data.define(:order_id, :product_id) ``` ##### 2. Create Subscribers A subscriber is a class that reacts to events. Include `Rage::Events::Subscriber`, declare what you subscribe to, and implement `call`: ```ruby class UpdateStock include Rage::Events::Subscriber subscribe_to OrderCreated def call(event) # `event` is an instance of `OrderCreated` end end ``` ##### 3. Publish Events When something happens, publish the event: ```ruby Rage::Events.publish(OrderCreated.new(order_id: 1, product_id: 2)) ``` That's it. The system handles the rest. info Event systems are designed to decouple components. Publishers don't know about subscribers, their logic, or their success/failure status. Each subscriber is an independent component responsible for its own error handling. Therefore, `Rage::Events.publish` **does not** propagate exceptions from subscribers. If a subscriber fails, it won't prevent other subscribers from executing or cause the `publish` call to raise an exception. #### Event Hierarchies: Model Your Domain You can subscribe to any class or module in an event's inheritance chain. This lets you model complex workflows naturally - without duplicating code or creating brittle dependencies. ##### The Problem Say `ProductViewed`, `ProductLiked`, and `ProductAddedToWishlist` should all trigger `UpdateRecommendations`. You could subscribe to each one: ```ruby class UpdateRecommendations include Rage::Events::Subscriber subscribe_to ProductViewed, ProductLiked, ProductAddedToWishlist end ``` But this can be error-prone and difficult to scale. What if you add `ProductShared`? What if multiple subscribers need the same set of events? ##### The Solution: Use Inheritance Define events with a common parent: ```ruby # Create a module that defines shared behavior module ProductInteractionEvent end # Define the events and include the module ProductViewed = Data.define(:product_id) do include ProductInteractionEvent end ProductLiked = Data.define(:product_id) do include ProductInteractionEvent end ProductAddedToWishlist = Data.define(:product_id) do include ProductInteractionEvent end ``` Now subscribe to the module: ```ruby class UpdateRecommendations include Rage::Events::Subscriber subscribe_to ProductInteractionEvent end ``` Adding a new product interaction event is now trivial - just include the module, and all relevant subscribers automatically react. ##### Handle Everything You can also subscribe to a base class to handle all events uniformly: ```ruby class ApplicationEvent < Data end ``` Use it for your events: ```ruby ProductViewed = ApplicationEvent.define(:product_id) do include ProductInteractionEvent end ``` And subscribe to the base class: ```ruby class StoreInDatabase include Rage::Events::Subscriber subscribe_to ApplicationEvent def call(event) # the event can be any event inherited from `ApplicationEvent` end end ``` Ruby's inheritance chain is the foundation of `Rage::Events`. It allows you to naturally define events, share behavior, and maintain clear boundaries. Subscribing to parent classes or modules introduces no runtime overhead - subscription lookups are cached, so you can structure your event hierarchies as complex as needed. #### Deferred Subscribers By default, subscribers execute synchronously when you publish an event. If a subscriber takes one second to complete, the `Rage::Events.publish` call will also take one second. To avoid blocking, mark subscribers as deferred: ```ruby class CreateShipment include Rage::Events::Subscriber subscribe_to OrderCreated, deferred: true end ``` Deferred subscribers are executed in the background using Rage's [background queue](https://rage-rb.dev/docs/deferred.md) and automatically retried if they fail. #### Error Handling Use `rescue_from` in your subscribers to handle exceptions in a centralized way: ```ruby class CreateShipment include Rage::Events::Subscriber subscribe_to OrderCreated, deferred: true rescue_from Net::HTTPError do |exception| Rage.logger.with_context(exception:) do Rage.logger.error "Shipment API is unavailable" end raise exception end end ``` info Remember to re-raise your exceptions in deferred subscribers, otherwise Rage will consider the subscriber successful and won't retry it. #### Context Sometimes you need to pass additional information with an event that doesn't belong to the event itself. Use the `context` parameter when publishing: ```ruby event = OrderCreated.new(order_id: 1, product_id: 2) Rage::Events.publish(event, context: { published_at: Time.now }) ``` Subscribers can access context data through the optional `context` keyword argument: ```ruby class UpdateStock include Rage::Events::Subscriber subscribe_to OrderCreated def call(event, context:) puts "Event published at: #{context[:published_at]}" end end ``` #### Visualizing Event Flow The biggest advantage of event-driven architecture - separation of concerns - is also one of its challenges. While publishers don't need to know about subscribers, developers do. The `rage events` CLI makes the implicit explicit. It shows you exactly what happens when an event is published by building a tree of event-subscriber relationships. Let's say we have the following events: ```ruby # Base event class class ApplicationEvent < Data end # Shared behavior module ProductInteractionEvent end # Event class OrderCreated = ApplicationEvent.define(:order_id, :product_id) do include ProductInteractionEvent end ``` And subscribers: ```ruby class StoreInDatabase include Rage::Events::Subscriber subscribe_to ApplicationEvent end class CreateShipment include Rage::Events::Subscriber subscribe_to OrderCreated end class UpdateRecommendations include Rage::Events::Subscriber subscribe_to ProductInteractionEvent end ``` Run the `rage events` command in your terminal to see what subscribers will be called when the `OrderCreated` event is published: ```text $ rage events ├─ OrderCreated │ ├─ CreateShipment │ ├─ ProductInteractionEvent │ │ └─ UpdateRecommendations │ └─ ApplicationEvent │ └─ StoreInDatabase ``` #### When Should You Use Events? Events are not for everything. Use them when: * **Behavior spans multiple concerns** - sending emails, updating analytics, creating shipments, notifying third parties * **Side effects evolve over time** - you want to add features without rewiring logic * **You want extensibility** - new reactions shouldn't require modifying existing code * **The system should explain itself** - "what happens when X occurs?" should have a clear answer Don't use them for: * Simple, single-purpose operations * Core business logic with a single responsibility * Cases where direct method calls are clearer If you've ever been afraid to touch a method because you didn't know what it would break - events are for you. #### Benchmarks The following benchmark measures the overhead an event publishing system introduces. The chart shows how much slower it is to trigger two subscribers by publishing an event versus calling them directly. Rage ```ruby # Define an event TestEvent = Data.define # Define subscribers class TestSubscriber1 include Rage::Events::Subscriber subscribe_to TestEvent def call(_) end end class TestSubscriber2 include Rage::Events::Subscriber subscribe_to TestEvent def call(_) end end # Run the benchmark require "benchmark/ips" RubyVM::YJIT.enable Benchmark.ips do |x| x.report("publish event") do Rage.events.publish(TestEvent.new) end x.report("call manually") do event = TestEvent.new TestSubscriber1.new.call(event) TestSubscriber2.new.call(event) end x.compare! end ``` Wisper ```ruby # Define notifiers class TestNotifier1 def self.process_test_event(_) = new.process_test_event(_) def process_test_event(_) end end class TestNotifier2 def self.process_test_event(_) = new.process_test_event(_) def process_test_event(_) end end # Define an event TestEvent = Data.define # Define the publisher class TestPublisher include Wisper::Publisher def call(event) broadcast(:process_test_event, event) end end # Create subscriptions publisher = TestPublisher.new publisher.subscribe(TestNotifier1) publisher.subscribe(TestNotifier2) # Run the benchmark require "benchmark/ips" RubyVM::YJIT.enable Benchmark.ips do |x| x.report("publish event") do publisher.call(TestEvent.new) end x.report("call manually") do event = TestEvent.new TestNotifier1.process_test_event(event) TestNotifier2.process_test_event(event) end x.compare! end ``` dry-events ```ruby require "dry/events/publisher" # Define listeners class TestListener1 def self.on_process_event(_) = new.on_process_event(_) def on_process_event(_) end end class TestListener2 def self.on_process_event(_) = new.on_process_event(_) def on_process_event(_) end end # Define the publisher class Publisher include Dry::Events::Publisher[:test_publisher] register_event("process.event") end # Create subscriptions publisher = Publisher.new publisher.subscribe(TestListener1) publisher.subscribe(TestListener2) # Run the benchmark require "benchmark/ips" RubyVM::YJIT.enable Benchmark.ips do |x| x.report("publish event") do publisher.publish("process.event", {}) end x.report("call manually") do event = {} TestListener1.on_process_event(event) TestListener2.on_process_event(event) end x.compare! end ``` --- ### Introduction #### Creating a New App Rage includes a built-in CLI utility that makes it easy to create and set up new applications. To get started, run the following commands: ```bash gem install rage-rb rage new my-app ``` This installs Rage and creates a new application in the `my-app` directory with all the necessary files and folder structure. Next, navigate to your application directory and install the required dependencies: ```bash cd my-app bundle install ``` Finally, start the development server: ```bash rage s ``` Your application is now running! Open your browser and visit `http://localhost:3000` to see it in action. ##### Database Configuration If you're planning to use a database, you can preconfigure your application during creation by using the `-d` option: ```bash rage new my-app -d postgresql ``` This sets up your application with the specified database adapter. Supported options include `mysql`, `trilogy`, `postgresql`, `sqlite3`. #### Project Structure When you create a new Rage application, several directories are automatically generated. Here's an overview of the key folders and their purposes: | Path | Description | | ----------------------- | ---------------------------------------------------------------------------------------------------- | | `app/` | Contains your application's core code (controllers, models, etc.) | | `config/application.rb` | Global framework configuration files | | `config/environments/` | Environment-specific framework configuration | | `config/initializers/` | Files that run during startup to configure gems and application code | | `config/routes.rb` | Defines your application's HTTP routes | | `lib/` | Reusable code that isn't application-specific, such as utilities or third-party service integrations | | `db/` | Stores database migrations and seed files | #### Active Record Integration When you use the `-d` option to specify a database, Rage automatically configures [Active Record](https://guides.rubyonrails.org/active_record_basics.html) as your ORM. This means you can create models, run migrations, and query your database without any additional setup. The `rage` CLI includes several commands for working with Active Record: ```bash # Generate a new model rage g model User # Run pending migrations rage db:migrate # Populate the database with seed data rage db:seed ``` #### Sequel Integration Rage also supports using Sequel instead of Active Record. To use Sequel as your ORM: 1. Create your application **without** the `-d` option 2. Manually configure Sequel in an initializer file Create a new initializer file with the following configuration: config/initializers/sequel.rb ```ruby Sequel.extension :fiber_concurrency DB = Sequel.connect("sqlite://my-app.db") ``` This example uses SQLite, but you can replace the connection string with your preferred database. #### CLI Commands Reference The `rage` CLI provides several helpful commands to streamline your development workflow: | Command | Description | | ----------------- | ------------------------------------------------- | | `rage s` | Starts the server | | `rage c` | Opens an interactive console for your application | | `rage routes` | Displays all defined routes | | `rage middleware` | Shows all enabled Rack middleware | --- ### Logging Rage provides a powerful structured logging system that makes it easy to debug your application and integrate with observability platforms. Unlike traditional text-based logs, Rage's logs are built on key-value pairs, making them searchable, filterable, and ready for analysis. This guide covers everything from basic logging patterns to advanced integration with external monitoring tools. #### Structured Logging All logs in Rage are structured as key-value pairs, making them easy to search, filter, and analyze. Additionally, every log entry has a list of tags associated with it. The first tag in every log entry is the current request ID. By default, logs are formatted as plain text in development and JSON in production. Here's a sample text log entry: ```text [fecbba0735355738] timestamp=2025-09-19T11:12:56+00:00 pid=1825 level=info message=hello ``` In this entry: * **Keys**: `timestamp`, `pid`, `level`, and `message` * **Tags**: `fecbba0735355738` (the request ID) Use the `Rage::Logger#tagged` method to add custom tags and `Rage::Logger#with_context` to add custom keys. ##### Best Practice: Consistent Log Messages When logging with Rage, your code should always log the same message regardless of input. This makes logs searchable and easier to analyze. ❌ **Avoid**: ```ruby def process_purchase(user_id:, product_id:) Rage.logger.info "processing purchase with user_id = #{user_id}; product_id = #{product_id}" end ``` This creates a unique message for each combination of inputs, making it difficult to search and aggregate logs. ✅ **Use this instead**: ```ruby def process_purchase(user_id:, product_id:) Rage.logger.info "processing purchase", user_id: user_id, product_id: product_id end ``` Now all purchases log the same message ("processing purchase"), with the variable data stored as structured keys. This allows you to easily search for all purchase logs and filter by specific user IDs or product IDs. #### Extending Request Logs By default, Rage logs each request with standard information like HTTP method, path, controller, action, status code, and duration. You can enrich these logs with custom data by defining the `append_info_to_payload` method in your controllers. ##### Adding Custom Keys Define `append_info_to_payload` in a specific controller to enrich only that controller's logs, or in `ApplicationController` to apply the change globally: app/controllers/application\_controller.rb ```ruby class ApplicationController < RageController::API private def append_info_to_payload(payload) payload[:response_size] = response.body.size end end ``` Now, request logs will include the additional `response_size` key: ```text [0c374wet9vquk00t] timestamp=2025-09-19T11:12:56+00:00 pid=1825 level=info method=GET path=/ controller=UsersController action=index response_size=123 status=200 duration=1.39 ``` ##### Common Use Cases * **Multi-tenancy**: adding a `tenant_id` or `account_id` * **User tracking**: adding a `user_id` for authenticated requests * **Request tracing**: adding correlation IDs from clients * **Performance monitoring**: adding custom timing metrics #### Global Log Context While `append_info_to_payload` extends request logs, you may want to add custom information to every log entry Rage produces. This could include trace and span IDs, the application environment, or the application's version. You can configure Rage to add custom log tags and context globally. For example, the following code uses `config.log_tags` to tag all logs with the current environment: ```ruby Rage.configure do config.log_tags << Rage.env end ``` Use `config.log_context` to add custom context to every log entry. The following example adds the `version` key to all log entries: ```ruby Rage.configure do config.log_context << { version: ENV["MY_APP_VERSION"] } end ``` Both `config.log_tags` and `config.log_context` also accept callables, which allows you to add dynamic information to your logs: ```ruby Rage.configure do config.log_context << proc do { trace_id: MyObservabilitySDK.trace_id, span_id: MyObservabilitySDK.span_id } end end ``` The callable should return a string for `config.log_tags`, a hash for `config.log_context`, or `nil`: ```ruby Rage.configure do config.log_context << proc do { trace_id: MyObservabilitySDK.trace_id } if MyObservabilitySDK.active? end end ``` warning If a callable passed to `config.log_tags` or `config.log_context` raises an exception, the request will fail. Make sure to handle exceptions within your callables if necessary. info If you're developing a gem that configures `log_tags` or `log_context`, it's a good practice to use constants for your callables. This allows users to easily remove them if needed. ```ruby module MyObservabilitySDK class LogContext def self.call # ... end end def self.install Rage.configure do config.log_context << LogContext end end end ``` Later, in the user's code: ```ruby Rage.configure do config.log_context.delete(MyObservabilitySDK::LogContext) end ``` #### External Loggers The standard Ruby logger is focused on text output, which is why many observability SDKs provide their own interfaces for sending structured logs to their platforms. Rage allows you to pipe its raw structured logging data directly to external observability tools without serializing it to text first. To do that, pass a callable to the [config.logger](https://api.rage-rb.dev/Rage/Configuration#logger=-instance_method) configuration option: ```ruby class MyExternalLogger def self.call(severity:, tags:, context:, message:, request_info:) # ... end end Rage.configure do config.logger = MyExternalLogger end ``` Now, Rage will call the `MyExternalLogger#call` method for every log entry produced by the application. Refer to the [API Documentation](https://api.rage-rb.dev/ExternalLoggerInterface) for the complete description of the arguments passed to the `#call` method. This feature allows you to match the interface of `Rage::Logger` with the interface of an external observability tool and gives you full control over your logging data. For example, here's how you might connect Rage's logger to Sentry: ```ruby # Define the external logger class class SentryLogger def call(severity:, tags:, context:, message:, request_info:) # Use logger context as structured data data = context # Add logger tags to the data tags.each do |tag| data = data.merge("tags.#{tag}" => "true") end # For request logs, add the HTTP path and customize the log message if request_info data[:path] = request_info[:env]["PATH_INFO"] message = "Request processed" end # Send the data to Sentry Sentry.logger.log(severity, message, parameters: [], **data) end end # Register the external logger Rage.configure do config.logger = SentryLogger.new end ``` --- ### OpenAPI Documentation Rage includes built-in support for generating OpenAPI specifications automatically from your controllers. `Rage::OpenAPI` analyzes your routes and controller code to create interactive API documentation. #### Try It First Before setting up your own project, try the [OpenAPI Playground](https://openapi-playground.rage-rb.dev) to see how `Rage::OpenAPI` works. Experiment with different tags and serializers to understand the features available. #### Setup Mount `Rage::OpenAPI` in your application to expose an OpenAPI specification endpoint. ##### Option 1: Mount in `config.ru` ```ruby map "/publicapi" do run Rage::OpenAPI.application end ``` ##### Option 2: Mount in Routes ```ruby Rage.routes.draw do mount Rage::OpenAPI.application, at: "/publicapi" end ``` #### Basic Usage Let's say you have these routes defined: ```ruby Rage.routes.draw do namespace :api do namespace :v1 do resources :users, only: %i[index show create] resources :photos, only: :index end namespace :v2 do resources :users, only: :show end end end ``` Start your server with `rage s` and visit `http://localhost:3000/publicapi`. You'll see an automatically generated specification listing all your endpoints: ![OpenAPI Example](/img/docs/openapi_1.webp)![OpenAPI Example](/img/docs/openapi_1_dark.webp) #### Documenting Your API While the auto-generated specification is a good start, you'll want to add descriptions, parameters, and response schemas to make it truly useful. ##### The Rage Approach `Rage::OpenAPI` uses a simple, focused approach: * **No complex DSL** - Instead of learning a complicated domain-specific language, you use familiar YARD-style tags * **Documentation in code** - Tags go directly in your controllers, keeping docs close to implementation * **Focused scope** - Covers the most common documentation needs without trying to expose every OpenAPI feature This approach means you spend less time learning tools and more time building your API. ##### How Tags Work You document your API using comment tags above controller actions: * **Action-level tags** (like `@description`) apply to a specific action * **Class-level tags** (like `@deprecated`) apply to all actions in that controller and any child controllers #### Available Tags ##### Summary Add a simple comment above your action to create a one-line summary: ```ruby class Api::V1::UsersController < ApplicationController # Returns the list of all active non-admin users. def index end end ``` The summary appears in your OpenAPI specification: ![OpenAPI Example](/img/docs/openapi_2.webp)![OpenAPI Example](/img/docs/openapi_2_dark.webp) ##### Description Use `@description` for longer, more detailed explanations. It supports Markdown formatting and can span multiple lines: ```ruby class Api::V1::UsersController < ApplicationController # Returns the list of all active non-admin users. # @description This endpoint provides access to all registered # users in the system. Only **non-admin users** are included # in the response. def show end end ``` ##### Version and Title Use `@version` and `@title` to set metadata for your entire API specification. These tags should appear only once and set the `title` and `version` properties of the [info object](https://swagger.io/docs/specification/v3_0/basic-structure/): ```ruby class ApplicationController < RageController::API # @version 1.0.0 # @title User Management API end ``` ##### Internal Notes Leave notes for other developers that won't appear in the public specification: ```ruby class Api::V1::UsersController < ApplicationController # @internal All changes to this action must be approved by a principal engineer. def create end end ``` ##### Deprecation Mark individual actions as deprecated: ```ruby class Api::V1::UsersController < ApplicationController def index end # @deprecated def create end end ``` Or mark an entire controller as deprecated (affects all actions and child controllers): ```ruby class Api::V1::UsersController < ApplicationController # @deprecated def index end def create end end ``` ##### Authentication Document your authentication requirements using the `@auth` tag. It automatically tracks which endpoints require authentication based on your `before_action` callbacks: ```ruby class ApplicationController < RageController::API before_action :authenticate_by_token # @auth authenticate_by_token private def authenticate_by_token # Authentication logic end end ``` `Rage::OpenAPI` automatically: * Tracks all uses of the `authenticate_by_token` callback * Respects `skip_before_action` calls * Applies the security scheme only to protected endpoints By default, it uses bearer token authentication: ```yaml type: http scheme: bearer ``` ###### Custom Security Schemes Customize the [security scheme](https://swagger.io/docs/specification/v3_0/authentication/) by adding YAML inline: ```ruby class ApplicationController < RageController::API before_action :authenticate_by_token # @auth authenticate_by_token # type: apiKey # in: header # name: X-API-Key end ``` ###### Multiple Security Schemes Support multiple authentication methods: ```ruby class ApplicationController < RageController::API before_action :authenticate_by_user_token before_action :authenticate_by_service_token # @auth authenticate_by_user_token # @auth authenticate_by_service_token end ``` ###### Custom Scheme Names Rename the security scheme in the specification: ```ruby class ApplicationController < RageController::API before_action :authenticate_by_token # @auth authenticate_by_token UserAuth end ``` ###### Shared Schemes You can also reference security schemes defined in your [shared components file](#shared-references): ```ruby class ApplicationController < RageController::API # @auth #/components/securitySchemes/BasicAuth end ``` ###### Per-Endpoint Scopes When using OAuth2 or OpenID Connect with shared security schemes, you can specify per-endpoint scopes using the `@auth_scope` tag. This is useful when different actions require different permission levels. Reference the security scheme by name: ```ruby class ApplicationController < RageController::API before_action :authenticate_user # @auth authenticate_user #/components/securitySchemes/OAuth2 end class Api::V1::UsersController < ApplicationController # @auth_scope OAuth2 [read:users] def index end # @auth_scope OAuth2 [read:users, write:users] def update end end ``` If your controller uses only one security scheme, you can omit the scheme name: ```ruby class Api::V1::UsersController < ApplicationController # @auth_scope [read:users] def index end # @auth_scope [read:users, write:users] def update end end ``` ##### Responses `Rage::OpenAPI` provides three ways to document response schemas: ###### 1. Inline Schema For simple responses, define the schema directly in the tag: ```ruby class Api::V1::UsersController < ApplicationController # @response { id: Integer, full_name: String, email: String } def show end end ``` ###### 2. Shared References For complex or reusable schemas, use [shared references](#shared-references). ###### 3. Automatic Schema Generation `Rage::OpenAPI` can automatically generate schemas from ActiveRecord models or [Alba](https://github.com/okuramasafumi/alba) serializers. Given this Alba resource: ```ruby class UserResource include Alba::Resource root_key :user attributes :id, :name, :email end ``` Reference it in your controller: ```ruby class Api::V1::UsersController < ApplicationController # @response UserResource def show end end ``` This generates: ```yaml schema: type: object properties: user: type: object properties: id: type: string name: type: string email: type: string ``` Most Alba features are supported, including associations, key transformations, inheritance, and typed attributes. ###### Collections Use `Array<>` or `[]` syntax for arrays: ```ruby class Api::V1::UsersController < ApplicationController # @response Array def index end end ``` ###### Namespace Resolution `Rage::OpenAPI` takes namespaces into account. If you have `Api::V1::UserResource` and `Api::V2::UserResource`, referencing `UserResource` from `Api::V2::UsersController` automatically uses `Api::V2::UserResource`. ###### Multiple Status Codes Document different responses for different status codes: ```ruby class Api::V1::UsersController < ApplicationController # @response 200 UserResource # @response 404 { error: String } def show end end ``` ###### Global Responses Apply responses to all actions in a controller and its children: ```ruby class Api::V1::BaseController < ApplicationController # @response 404 { status: "NOT_FOUND" } # @response 500 { status: "ERROR", message: String } end ``` ##### Request Bodies The `@request` tag works similarly to `@response`. It accepts inline schemas, [shared references](#shared-references), or ActiveRecord models. When using ActiveRecord models, `Rage::OpenAPI` automatically excludes internal attributes like `id`, `created_at`, `updated_at`, and `type`: ```ruby class Api::V1::UsersController < ApplicationController # @request User def create end end ``` You can also use inline schemas: ```ruby class Api::V1::UsersController < ApplicationController # @request { name: String, email: String, password: String } def create end end ``` ##### Query Parameters Document query parameters using the `@param` tag: ```ruby class Api::V1::UsersController < ApplicationController # @param account_id The account the records are attached to def index end end ``` ###### Optional Parameters Add `?` after the parameter name to mark it as optional: ```ruby class Api::V1::UsersController < ApplicationController # @param created_at? Filter records by creation date def index end end ``` ###### Parameter Types Specify types using `{}` syntax: ```ruby class Api::V1::UsersController < ApplicationController # @param is_active {Boolean} Filter records by active status # @param page {Integer} Page number for pagination # @param per_page? {Integer} Items per page def index end end ``` #### Shared References For complex or reusable schemas, create a file with [shared OpenAPI component definitions](https://swagger.io/docs/specification/v3_0/components/). This file can be in YAML or JSON format and must have a root `components` key. Create `config/openapi_components.yml`: ```yaml components: schemas: User: type: object properties: id: type: integer name: type: string email: type: string format: email Error: type: object properties: message: type: string code: type: string ``` Reference these components in your controllers using JSON Pointer syntax: ```ruby class Api::V1::UsersController < ApplicationController # @response 200 #/components/schemas/User # @response 404 #/components/schemas/Error def show end end ``` #### Controlling Visibility Sometimes you want to hide certain endpoints from your public API documentation. `Rage::OpenAPI` provides two ways to control visibility. ##### Using `@private` Hide individual actions: ```ruby class Api::V1::UsersController < ApplicationController def show end # @private def create end end ``` Or hide entire controllers: ```ruby class Api::V1::UsersController < ApplicationController # @private def show end def create end end ``` ##### Namespace Filtering Limit the specification to a specific namespace: ```ruby map "/publicapi" do run Rage::OpenAPI.application(namespace: "Api::V2") end ``` This only includes controllers under `Api::V2::`. ##### Multiple Specifications Create different specifications for different audiences: ```ruby map "/publicapi" do run Rage::OpenAPI.application(namespace: "Api::Public") end map "/internalapi" do use Rack::Auth::Basic do |user, password| user == "admin" && password == ENV["ADMIN_PASSWORD"] end run Rage::OpenAPI.application(namespace: "Api::Internal") end ``` #### Custom Tag Organization By default, `Rage::OpenAPI` groups endpoints by their controller namespace. For example, `Api::V1::UsersController` actions are grouped under the `v1/Users` tag. You can customize this grouping with a tag resolver: ```ruby Rage.configure do config.openapi.tag_resolver = proc do |controller, action, default_tag| # Custom logic here end end ``` The resolver receives three arguments: * `controller` - The controller class * `action` - The action name (symbol) * `default_tag` - The original tag generated by `Rage::OpenAPI` The resolver should return a string or array of strings representing the tag(s) for the endpoint. ##### Example: Multiple Tags ```ruby Rage.configure do config.openapi.tag_resolver = proc do |controller, action, default_tag| tags = [default_tag] if controller.name.include?("Admin") tags << "Admin" elsif controller.name.include?("Public") tags << "Public API" end tags end end ``` --- ### Rails Integration You can integrate Rage into existing Rails applications to boost performance while keeping your familiar Rails stack. This guide walks you through the process using a real-world example. #### Example Application We'll use [rage-rb/rails-integration-app](https://github.com/rage-rb/rails-integration-app) - a warehouse booking system for shipping companies. The app has three stages: 1. Welcome page - select day and booking duration 2. Available slots - choose a specific time slot 3. Confirmation page The complete integration is available in [PR #2](https://github.com/rage-rb/rails-integration-app/pull/2). #### Integration Goal After integration, Rage will handle HTTP requests, while Rails continues to manage code loading, ActiveRecord, migrations, and other framework features. You get Rage's performance with Rails' ecosystem. #### Integration Steps ##### Step 1: Install Rage Add Rage to your `Gemfile`: Gemfile ```ruby gem "rage-rb" ``` Run `bundle install` to install the gem. Next, require Rage in your application configuration: config/application.rb ```ruby # ... existing Rails configuration ... end # IMPORTANT: Require this AFTER defining your Rails application class require "rage/rails" ``` ##### Step 2: Update Routes Change your routes file to use Rage: config/routes.rb ```ruby Rage.routes.draw do resources :bookings, only: %i(index create) end ``` ##### Step 3: Update Controllers Change your base controller to inherit from `RageController::API`: app/controllers/application\_controller.rb ```ruby class ApplicationController < RageController::API end ``` All controllers that inherit from `ApplicationController` will automatically use Rage. ##### Step 4: Update Rack Configuration Modify `config.ru` to run Rage instead of Rails: config.ru ```ruby require_relative "config/environment" run Rage.application ``` ##### Step 5: Update Server Start Command Rage uses its own server command. Update your start scripts: **Development:** ```bash bundle exec rage s ``` **Production (Docker example):** Dockerfile ```dockerfile CMD bundle exec rage s -b 0.0.0.0 ``` ##### Step 6: Configure Rage Configure Rage to match your application's needs. Configuration must happen in the `Rails.configuration.after_initialize` block: config/application.rb ```ruby Rails.configuration.after_initialize do Rage.configure do # Match your Puma worker count (optional) config.server.workers_count = 1 # Add required middleware config.middleware.use ActionDispatch::HostAuthorization # Environment-specific middleware if Rails.env.development? config.middleware.use ActiveRecord::Migration::CheckPending end end end ``` warning Rage has its own middleware stack and **does not** automatically copy Rails middleware. You need to explicitly configure any Rails middleware your application requires. Also, let's update the CORS middleware. Initially, the application was using `Rack::Cors`. And while Rage is fully compatible with `Rack::Cors`, for simpler cases it is recommended to use the built-in [Rage::Cors](https://api.rage-rb.dev/Rage/Cors) middleware. ```diff --- a/config/application.rb +++ b/config/application.rb @@ -35,16 +35,6 @@ module SlotBooking # Middleware like session, flash, cookies can be added back manually. # Skip views, helpers and assets when generating a new resource. config.api_only = true - - config.middleware.insert_before 0, Rack::Cors do - allow do - origins "localhost:5173", "https://clumsy-squirrel-4315.pages.dev" - - resource "*", - headers: :any, - methods: [:get, :post, :put, :patch, :delete, :options, :head] - end - end end end @@ -55,6 +45,10 @@ Rails.configuration.after_initialize do config.server.workers_count = 1 config.server.port = 3000 + config.middleware.use Rage::Cors do + allow "localhost:5173", "https://clumsy-squirrel-4315.pages.dev" + end + config.middleware.use ActionDispatch::HostAuthorization if Rails.env.development? config.middleware.use ActionDispatch::Reloader ``` That's it! You've successfully integrated Rage. Now let's look at the performance improvements. #### Performance Benchmarks To measure the impact, we ran load tests using [this k6 script](https://github.com/rage-rb/rails-integration-app/blob/main/benchmark/k6.js) that simulates real-world traffic patterns. **Test Environment:** * Ruby 3.2.2 * AWS EC2 t2.medium instance * Same application code, only the web server differs **Rails results** ```text /\ |‾‾| /‾‾/ /‾‾/ /\ / \ | |/ / / / / \/ \ | ( / ‾‾\ / \ | |\ \ | (‾) | / __________ \ |__| \__\ \_____/ .io execution: local script: k6.js output: - scenarios: (100.00%) 1 scenario, 50 max VUs, 1m30s max duration (incl. graceful stop): * bookings: 25.00 iterations/s for 1m0s (maxVUs: 50, gracefulStop: 30s) ✓ response code was 200 ✓ response code was 200 or 409 ✓ CORS header is present checks.........................: 100.00% ✓ 1757 ✗ 0 data_received..................: 1.4 MB 23 kB/s data_sent......................: 212 kB 3.4 kB/s http_req_blocked...............: avg=19.03µs min=1.45µs med=7.81µs max=992.64µs p(90)=11.31µs p(95)=27.25µs http_req_connecting............: avg=6.63µs min=0s med=0s max=469.75µs p(90)=0s p(95)=0s http_req_duration..............: avg=949.76ms min=3.77ms med=825.51ms max=59.78s p(90)=1.72s p(95)=2.4s { expected_response:true }...: avg=949.76ms min=3.77ms med=825.51ms max=59.78s p(90)=1.72s p(95)=2.4s http_req_failed................: 0.00% ✓ 0 ✗ 1629 http_req_receiving.............: avg=86.57µs min=38.1µs med=83.14µs max=1.02ms p(90)=105.64µs p(95)=117.76µs http_req_sending...............: avg=35.78µs min=14.57µs med=34.59µs max=243.32µs p(90)=42.98µs p(95)=55.25µs http_req_tls_handshaking.......: avg=0s min=0s med=0s max=0s p(90)=0s p(95)=0s http_req_waiting...............: avg=949.64ms min=3.69ms med=825.41ms max=59.78s p(90)=1.72s p(95)=2.4s http_reqs......................: 1629 26.333117/s iteration_duration.............: avg=1.03s min=4.92ms med=918.01ms max=59.79s p(90)=1.75s p(95)=2.57s iterations.....................: 1501 24.263971/s vus............................: 22 min=1 max=45 vus_max........................: 50 min=50 max=50 running (1m01.9s), 00/50 VUs, 1501 complete and 0 interrupted iterations bookings ✓ [======================================] 00/50 VUs 1m0s 25.00 iters/s ``` **Rage results** ```text /\ |‾‾| /‾‾/ /‾‾/ /\ / \ | |/ / / / / \/ \ | ( / ‾‾\ / \ | |\ \ | (‾) | / __________ \ |__| \__\ \_____/ .io execution: local script: k6.js output: - scenarios: (100.00%) 1 scenario, 50 max VUs, 1m30s max duration (incl. graceful stop): * bookings: 25.00 iterations/s for 1m0s (maxVUs: 50, gracefulStop: 30s) ✓ response code was 200 ✓ response code was 200 or 409 ✓ CORS header is present checks.........................: 100.00% ✓ 1868 ✗ 0 data_received..................: 1.6 MB 26 kB/s data_sent......................: 229 kB 3.8 kB/s http_req_blocked...............: avg=17.38µs min=673ns med=7.95µs max=428.2µs p(90)=11.61µs p(95)=26.63µs http_req_connecting............: avg=6.2µs min=0s med=0s max=329.57µs p(90)=0s p(95)=0s http_req_duration..............: avg=4.13ms min=2.96ms med=3.96ms max=30.31ms p(90)=4.77ms p(95)=5.11ms { expected_response:true }...: avg=4.13ms min=2.96ms med=3.96ms max=30.31ms p(90)=4.77ms p(95)=5.11ms http_req_failed................: 0.00% ✓ 0 ✗ 1684 http_req_receiving.............: avg=76.82µs min=35.88µs med=76.92µs max=197.13µs p(90)=90.17µs p(95)=96.37µs http_req_sending...............: avg=37.46µs min=12.88µs med=35.39µs max=120.42µs p(90)=42.3µs p(95)=59.27µs http_req_tls_handshaking.......: avg=0s min=0s med=0s max=0s p(90)=0s p(95)=0s http_req_waiting...............: avg=4.01ms min=2.82ms med=3.85ms max=30.04ms p(90)=4.66ms p(95)=4.98ms http_reqs......................: 1684 28.066327/s iteration_duration.............: avg=4.93ms min=3.3ms med=4.32ms max=31.13ms p(90)=7.84ms p(95)=8.53ms iterations.....................: 1500 24.999697/s vus............................: 0 min=0 max=1 vus_max........................: 50 min=50 max=50 running (1m00.0s), 00/50 VUs, 1500 complete and 0 interrupted iterations bookings ✓ [======================================] 00/50 VUs 1m0s 25.00 iters/s ``` ##### Results While Rails latency degrades under load (p95 reaches 2.4s), Rage maintains consistent performance (~5ms). This means Rage handles traffic spikes gracefully using the same hardware. #### Gradual Migration with Multi-App Mode Rage supports running alongside Rails in the same application, allowing you to: * Migrate controller by controller * Keep Rails views for full-stack apps while using Rage for API endpoints * Test Rage's performance on specific endpoints before full migration ##### Setup 1. Perform all the integration steps updating only the controllers you want to migrate to inherit from `RageController::API` 2. Leave other controllers inheriting from `ActionController::API` (or `ActionController::Base`) 3. Update `config.ru` to use multi-app mode: ```ruby run Rage.multi_application ``` Rage automatically routes requests to the appropriate framework based on which controller handles the route. This gives you the flexibility to adopt Rage incrementally without risking your entire application. --- ### Routing Routing determines how your application responds to incoming HTTP requests. In Rage, routes connect URLs to controller actions and are defined in the `config/routes.rb` file. When a request comes in, Rage's router matches it against your defined routes and dispatches it to the appropriate controller action. info Refer to the [API Documentation](https://api.rage-rb.dev/Rage/Router/DSL/Handler) for a complete set of router methods. #### Resource Routing The `resources` helper is the quickest way to create routes for RESTful controllers. It automatically generates all five standard REST routes with a single line of code. Add this to your routes file: config/routes.rb ```ruby Rage.routes.draw do resources :users end ``` This single declaration creates all the routes you need for a typical REST resource: | HTTP Method | URL | Controller | Action | Purpose | | ----------------- | ------------ | ----------------- | --------- | -------------------- | | **GET** | `/users` | `UsersController` | `index` | List all users | | **POST** | `/users` | `UsersController` | `create` | Create a new user | | **GET** | `/users/:id` | `UsersController` | `show` | Show a specific user | | **PATCH**/**PUT** | `/users/:id` | `UsersController` | `update` | Update a user | | **DELETE** | `/users/:id` | `UsersController` | `destroy` | Delete a user | Routes with `:id` in the URL automatically make that value available in your controller via `params[:id]`. ##### Limiting Generated Routes Sometimes you don't need all five REST actions. Use the `:only` option to generate only the routes you need: config/routes.rb ```ruby Rage.routes.draw do resources :users, only: [:index, :show] end ``` This creates only two routes instead of five: | HTTP Method | URL | Controller | Action | | ----------- | ------------ | ----------------- | ------- | | **GET** | `/users` | `UsersController` | `index` | | **GET** | `/users/:id` | `UsersController` | `show` | Alternatively, you can use `:except` to exclude specific actions: config/routes.rb ```ruby Rage.routes.draw do resources :users, except: [:destroy] end ``` This generates all routes except `destroy`, which might be useful if you want to prevent users from being deleted through the API. ##### Namespaces Namespaces help you organize routes into logical groups, which is especially useful for API versioning or admin sections. They prefix both the URL path and the controller module: config/routes.rb ```ruby Rage.routes.draw do namespace :v1 do resources :users end end ``` This generates routes under the `/v1` path and expects controllers in the `V1::` module: | HTTP Method | URL | Controller | Action | | ----------------- | --------------- | --------------------- | --------- | | **GET** | `/v1/users` | `V1::UsersController` | `index` | | **POST** | `/v1/users` | `V1::UsersController` | `create` | | **GET** | `/v1/users/:id` | `V1::UsersController` | `show` | | **PATCH**/**PUT** | `/v1/users/:id` | `V1::UsersController` | `update` | | **DELETE** | `/v1/users/:id` | `V1::UsersController` | `destroy` | Your controller file would be located at `app/controllers/v1/users_controller.rb` and defined as `class V1::UsersController`. #### Custom Routing While `resources` handles most cases, sometimes you need more control over your routes. Rage provides HTTP method helpers (`get`, `post`, `put`, `patch`, `delete`) for defining custom routes. Here's an example controller with a custom action: app/controllers/user\_messages\_controller.rb ```ruby class UserMessagesController < ApplicationController def mark_as_read message = Message.find_by(id: params[:message_id], user_id: params[:user_id]) message.mark_as_read end end ``` You can map this to a custom URL like this: config/routes.rb ```ruby Rage.routes.draw do post "/messages/:user_id/:message_id/read", to: "user_messages#mark_as_read" end ``` Breaking this down: * `post` - specifies the HTTP method * `"/messages/:user_id/:message_id/read"` - the URL pattern (`:user_id` and `:message_id` become parameters) * `to: "user_messages#mark_as_read"` - the controller and action, separated by `#` Note that the controller name should be underscored and without the `Controller` suffix (e.g., `user_messages` instead of `UserMessagesController`). --- ### Testing with RSpec Rage provides built-in RSpec integration for testing your API endpoints. After requiring `rage/rspec`, you get access to familiar Rails-style testing helpers. #### Available Methods ##### HTTP Request Helpers ```ruby get(path, params: {}, headers: {}) options(path, params: {}, headers: {}) head(path, params: {}, headers: {}) post(path, params: {}, headers: {}, as: nil) put(path, params: {}, headers: {}, as: nil) patch(path, params: {}, headers: {}, as: nil) delete(path, params: {}, headers: {}, as: nil) ``` ##### Response Matchers * `have_http_status` - Test response status codes * `response.parsed_body` - Access parsed response body These helpers are available in request specs (`type: :request`) after requiring `rage/rspec`. #### Setup Guide ##### Step 1: Install Dependencies Add RSpec and Database Cleaner to your `Gemfile`: Gemfile ```ruby group :test do gem "rspec" gem "database_cleaner-active_record" end ``` Run `bundle install` to install the gems. info This example uses ActiveRecord. If you're using a different ORM, choose the appropriate [Database Cleaner adapter](https://github.com/DatabaseCleaner/database_cleaner). ##### Step 2: Initialize RSpec Run RSpec's initialization command to create configuration files: ```bash rspec --init ``` This creates `spec/spec_helper.rb` and `.rspec` configuration files. ##### Step 3: Configure Database Cleaner Add these hooks to `spec/spec_helper.rb` inside the `RSpec.configure` block: spec/spec\_helper.rb ```ruby RSpec.configure do |config| # Configure truncation strategy config.before(:suite) do DatabaseCleaner.strategy = :truncation end # Clean database between tests config.around(:each) do |example| DatabaseCleaner.cleaning(&example) end end ``` Important The `transaction` cleaning strategy is currently not supported. This is because it relies on Database Cleaner starting a transaction on a connection and expecting the same connection to be used to create the records and query them in the controller. However, Rage executes requests in separate Fibers, causing Active Record to use different connections for the test and the controller, thus breaking this expectation. ##### Step 4: Write Your First Test Create a test file and require `rage/rspec`: spec/requests/home\_spec.rb ```ruby require "rage/rspec" RSpec.describe "Home", type: :request do it "returns a successful response" do get "/" expect(response).to have_http_status(:ok) expect(response.parsed_body).to eq("It works!") end end ``` Run your tests: ```bash rspec ``` #### Testing Examples ##### Basic GET Request with Headers Test an endpoint that requires authentication: spec/requests/api/v1/photos\_spec.rb ```ruby require "rage/rspec" RSpec.describe "Photos API", type: :request do describe "GET /api/v1/photos" do it "returns all photos" do get "/api/v1/photos", headers: { "Authorization" => "Bearer my-test-token" } expect(response).to have_http_status(:ok) expect(response.parsed_body.count).to eq(5) end end end ``` ##### Sending JSON Data Test creating resources with JSON payloads: spec/requests/api/v1/photos\_spec.rb ```ruby require "rage/rspec" RSpec.describe "Photos API", type: :request do describe "POST /api/v1/photos" do let(:photo_params) do { data: "base64_encoded_image", caption: "Beautiful sunset" } end it "creates a photo" do post "/api/v1/photos", params: photo_params, as: :json expect(response).to have_http_status(:created) end it "returns the newly created photo" do post "/api/v1/photos", params: photo_params, as: :json expect(response.parsed_body["data"]).to be_present expect(response.parsed_body["caption"]).to eq("Beautiful sunset") end end end ``` ##### Testing Subdomain Constraints Test routes that are constrained to specific subdomains: spec/requests/api/v1/photos\_spec.rb ```ruby require "rage/rspec" RSpec.describe "Photos API", type: :request do before { host! "api.example.com" } it "returns all photos" do get "/api/v1/photos" expect(response).to have_http_status(:ok) expect(response.parsed_body.count).to eq(5) end end ``` #### Testing Rage::Cable Rage provides RSpec helpers for testing WebSocket channels. These helpers are similar to Rails Action Cable testing helpers with minor differences. ##### Available Methods ###### Connection Helpers ```ruby connect(path, headers: {}) # Emulate a WebSocket connection stub_connection(identifiers) # Stub connection identifiers ``` ###### Channel Helpers ```ruby subscribe(params = {}) # Subscribe to the channel perform(action, data = {}) # Perform a channel action ``` ###### Matchers ```ruby expect(subscription).to be_confirmed expect(subscription).to have_stream_from("stream_name") expect(subscription).to have_stream_for(model) expect(connection).to be_rejected ``` ###### Accessors ```ruby subscription # The current subscription instance transmissions # Array of messages transmitted to the client connection # The current connection instance ``` ##### Testing Subscriptions Test that a channel correctly subscribes and sets up streams: spec/channels/chat\_channel\_spec.rb ```ruby require "rage/rspec" RSpec.describe ChatChannel, type: :channel do let(:user) { create(:user) } it "subscribes to the channel" do stub_connection(current_user: user) subscribe(room_id: 42) expect(subscription).to be_confirmed expect(subscription).to have_stream_from("chat_room_42") end it "streams for a specific model" do stub_connection(current_user: user) subscribe expect(subscription).to have_stream_for(user) end end ``` ##### Testing Transmissions Test messages sent to the client: spec/channels/notifications\_channel\_spec.rb ```ruby require "rage/rspec" RSpec.describe NotificationsChannel, type: :channel do it "sends a welcome message on subscribe" do subscribe expect(subscription).to be_confirmed expect(transmissions.last).to eq({ "message" => "welcome" }) end end ``` ##### Testing Broadcasts To test broadcasts, use RSpec's native mocking on `Rage::Cable.broadcast`: spec/channels/chat\_channel\_spec.rb ```ruby require "rage/rspec" RSpec.describe ChatChannel, type: :channel do it "broadcasts messages to the channel" do subscribe expect(Rage::Cable).to receive(:broadcast).with("chat", { message: "hello" }) perform :send_message, { message: "hello" } end end ``` info Unlike Rails, Rage does not provide the `have_broadcasted_to` matcher. Use `expect(Rage::Cable).to receive(:broadcast)` instead. ##### Testing Channel Actions Test custom actions defined in your channel: spec/channels/chat\_channel\_spec.rb ```ruby require "rage/rspec" RSpec.describe ChatChannel, type: :channel do it "broadcasts typing indicator" do subscribe expect(Rage::Cable).to receive(:broadcast).with("chat", { status: "typing" }) perform :typing end it "uses connection identifiers in actions" do stub_connection(current_user: "user_123") subscribe(room_id: 456) expect(Rage::Cable).to receive(:broadcast).with("info", { room_id: 456, user: "user_123" }) perform :info end end ``` ##### Testing Connections Test connection authentication and rejection: spec/channels/application\_cable\_spec.rb ```ruby require "rage/rspec" RSpec.describe ApplicationCable::Connection, type: :channel do it "connects with valid headers" do connect "/cable", headers: { "Authorization" => "Bearer valid_token" } expect(connection).not_to be_rejected expect(connection.current_user).to eq("authenticated_user") end it "rejects connections without credentials" do connect "/cable" expect(connection).to be_rejected end end ``` ##### Testing with Cookies and Session Use `cookies` and `session` helpers to set authentication data: spec/channels/application\_cable\_spec.rb ```ruby require "rage/rspec" RSpec.describe ApplicationCable::Connection, type: :channel do it "connects with cookies" do cookies["auth_token"] = "abc123" connect "/cable" expect(connection).not_to be_rejected end it "connects with encrypted cookies" do cookies.encrypted["user_id"] = "999" connect "/cable" expect(connection.current_user).to eq("user_999") end it "connects with session data" do session[:user_id] = "123" connect "/cable" expect(connection.current_user).to eq("123") end end ``` ##### Testing with Query Parameters Pass query parameters through the connection path: spec/channels/application\_cable\_spec.rb ```ruby require "rage/rspec" RSpec.describe ApplicationCable::Connection, type: :channel do it "uses query parameters for identification" do connect "/cable?token=secret123" expect(connection).not_to be_rejected expect(connection.current_user).to eq("user_from_token") end end ``` --- ### Server-Sent Events Server-Sent Events provide a simple way to push real-time updates from your server to clients over HTTP. Unlike WebSockets, SSE is unidirectional (server to client only), making it perfect for live feeds, notifications, and streaming responses. Rage supports three SSE patterns: * **Streams** - Send multiple updates over time using enumerators * **One-off updates** - Send a single message and close the connection * **Unbounded streams** - Long-lived connections with broadcast support #### Streaming with Enumerators The most common pattern is streaming data using Ruby enumerators. Rage reads from the enumerator and sends each value as an SSE message: ```ruby class MessagesController < RageController::API def index stream = Enumerator.new do |y| "Hello, world!".each_char do |char| sleep 1 y << char end end render sse: stream end end ``` Once the enumerator finishes, Rage automatically closes the SSE connection. ##### Streaming from External Sources Enumerators work great with external data sources like Redis, databases, or message queues: ```ruby class MessagesController < RageController::API def index redis = Redis.new stream = Enumerator.new do |y| loop do _, message = redis.blpop("messages", timeout: 5) break if message == "close" y << message end ensure redis.close end render sse: stream end end ``` tip Yielding `nil` values is safe - Rage ignores them. This lets you implement polling loops that only send data when it's available. ##### Streaming Objects When you yield objects (hashes, arrays, or other Ruby objects), Rage automatically converts them to JSON: ```ruby redis = Redis.new stream = Enumerator.new do |y| loop do _, message = redis.blpop("events") break if message == "close" y << { event: "message", data: message, timestamp: Time.now.to_i } end end render sse: stream ``` ##### SSE Fields The SSE protocol supports several fields beyond the data payload: `id`, `event`, and `retry`. Use `Rage::SSE.message` to include these fields: ```ruby file = File.new(params[:file], "r") stream = file.each_line.with_index.lazy.map do |line, i| Rage::SSE.message(line, id: i, event: "line", retry: 100) end render sse: stream ``` This sends messages like: ```text id: 0 event: line retry: 100 data: First line of the file id: 1 event: line retry: 100 data: Second line of the file ``` You can also combine SSE fields with object streaming: ```ruby redis = Redis.new stream = Enumerator.new do |y| loop do _, message = redis.blpop("notifications") break if message == "close" y << Rage::SSE.message({ notification: message }, event: "notification") end end render sse: stream ``` ##### Connection Keep-Alive Rage automatically sends periodic `: ping` comments to keep SSE connections alive. This prevents proxies and load balancers from closing idle connections. ##### Graceful Shutdown When the server restarts, Rage waits up to 15 seconds for active enumerator streams to finish. This gives your streams time to complete their work and clean up resources gracefully. info [Unbounded streams](#unbounded-streams) are interrupted immediately on restart since they have no natural end point. #### One-Off Updates For simple cases where you need to send a single message, pass any value directly to `render sse:`: ```ruby user = User.find(params[:id]) render sse: user ``` Rage sends the response and closes the connection immediately. This is useful for endpoints that return a single result but want to use the SSE format for consistency with other streaming endpoints. #### Unbounded Streams Unbounded streams let you create long-lived SSE connections that receive broadcasts from anywhere in your application. Instead of managing the connection yourself, you attach it to a named stream and broadcast messages to that stream. ##### Setting Up a Stream Use `Rage::SSE.stream` to create an unbounded stream: ```ruby render sse: Rage::SSE.stream("notifications-#{params[:user_id]}") ``` This sets up a persistent SSE connection attached to the `notifications-123` stream (assuming `user_id` is 123). The connection stays open until the client disconnects or you explicitly close it. ##### Broadcasting Messages Send messages to all connections on a stream using `Rage::SSE.broadcast`: ```ruby Rage::SSE.broadcast("notifications-#{user.id}", user.notifications.last) ``` Every client connected to `notifications-123` receives the notification. Broadcasts work from anywhere in your application, including controllers, models, and background tasks built with [Rage::Deferred](https://rage-rb.dev/docs/deferred.md). You can include SSE fields in broadcasts: ```ruby notification = user.notifications.last Rage::SSE.broadcast( "notifications-#{user.id}", Rage::SSE.message(notification, id: notification.id, event: "notification") ) ``` ##### Closing Streams Close all connections on a stream from the server side: ```ruby Rage::SSE.close_stream("notifications-#{user.id}") ``` ##### Composite Keys For better organization, use arrays as stream identifiers: ```ruby # In your controller render sse: Rage::SSE.stream([:notifications, params[:user_id]]) # Broadcasting Rage::SSE.broadcast([:notifications, user.id], user.notifications.last) # Closing Rage::SSE.close_stream([:notifications, user.id]) ``` #### Multi-Server Setup For deployments with multiple servers, or to enable broadcasts from external systems like Sidekiq, use the Redis adapter to synchronize streams across servers. ##### Configuration Create a `config/pubsub.yml` file: config/pubsub.yml ```yaml development: adapter: redis url: redis://localhost:6379 production: adapter: redis url: <%= ENV["REDIS_URL"] %> timeout: 0.2 ``` With this configuration, calls to `Rage::SSE.broadcast` and `Rage::SSE.close_stream` are synchronized across all servers. Clients connected to the same stream on different servers will receive all broadcasts to that stream. ##### Buffering There are situations where you need to broadcast to a stream before the connection is fully established. For example, a background job might start sending messages before the HTTP response begins streaming: ```ruby # This can cause lost messages! FetchNotifications.perform_async # may call Rage::SSE.broadcast("notifications", ...) render sse: Rage::SSE.stream("notifications") ``` To prevent message loss, create the stream object first. Rage buffers messages to known streams until the connection is established: ```ruby # Create the stream first - messages will be buffered stream = Rage::SSE.stream("notifications") FetchNotifications.perform_async # broadcasts are now buffered render sse: stream # establishes the connection and sends buffered messages ``` #### Low-Level Access For advanced use cases or gem authors who need full control over the SSE connection, use a proc: ```ruby render sse: ->(connection) do connection.write("data: Hello, world!\n\n") ensure connection.close end ``` With procs, you're responsible for: * Formatting messages according to the SSE protocol * Managing the connection lifecycle * Closing the connection when done This is primarily intended for libraries that need to integrate with Rage's SSE system. --- ### Telemetry Rage provides a built-in telemetry system that lets you observe and measure what's happening inside your application. Use it to integrate with monitoring platforms, track performance metrics, debug production issues, or build custom observability solutions. This guide covers how to instrument your application using Rage's telemetry API and integrate with external observability tools. #### Understanding Spans Rage's telemetry is built around **spans** - instrumentation points that wrap specific framework operations like controller actions, cable actions, deferred tasks, and fiber scheduling. Each span has a name following the `component.entity.action` pattern (for example, `controller.action.process`). You create handlers to observe these spans and integrate with your observability tools of choice. #### Creating Handlers You can observe spans by creating handlers that inherit from `Rage::Telemetry::Handler`. Here's a handler that notifies an external observability platform whenever a controller action takes more than 2 seconds: app/telemetry/controller\_duration\_handler.rb ```ruby class ControllerDurationHandler < Rage::Telemetry::Handler handle "controller.action.process", with: :monitor_duration def self.monitor_duration(name:) start = Time.now.to_i yield if Time.now.to_i - start > 2 MyObservabilitySDK.notify("Operation #{name} took more than 2 seconds") end end end ``` Breaking this down: 1. Create a class that inherits from `Rage::Telemetry::Handler` 2. Call `handle` with the span name (`controller.action.process`) and the method that will handle it (`monitor_duration`) 3. Define the handler method, which receives the operation name as a keyword argument 4. Start timing and pass control to the observed operation via `yield` 5. After the operation completes, check the duration and send a notification if needed ##### How Yield Works Each handler method controls when to pass control to the observed operation via `yield`. This design allows natural instrumentation patterns: * **Before operation**: Code before `yield` runs before the observed operation * **After operation**: Code after `yield` runs after the observed operation completes warning Your handlers should always call `yield`. Telemetry handlers are passive observers that shouldn't change application behavior. If your handler doesn't call `yield`, Rage will automatically call it for you, ensuring unstable or buggy observability code cannot break your application. #### Registering Handlers Register your handlers using the [config.telemetry](https://api.rage-rb.dev/Rage/Configuration/Telemetry) configuration option: config/application.rb ```ruby Rage.configure do config.telemetry.use ControllerDurationHandler end ``` If your handler needs initialization, you can also register instances: app/telemetry/controller\_duration\_handler.rb ```ruby class ControllerDurationHandler < Rage::Telemetry::Handler handle "controller.action.process", with: :monitor_duration def initialize(threshold:) @threshold = threshold end def monitor_duration(name:) start = Time.now.to_i yield if Time.now.to_i - start > @threshold MyObservabilitySDK.notify("Operation #{name} took more than #{@threshold} seconds") end end end ``` Register the initialized handler: config/application.rb ```ruby Rage.configure do config.telemetry.use ControllerDurationHandler.new(threshold: 2) end ``` #### Handler Arguments Handlers receive relevant context for each span through keyword arguments. Rage automatically detects which parameters your handler accepts and only passes those. You can enhance handlers by requesting additional context. Here's how to include the request URL in notifications: app/telemetry/controller\_duration\_handler.rb ```ruby class ControllerDurationHandler < Rage::Telemetry::Handler handle "controller.action.process", with: :monitor_duration def self.monitor_duration(name:, request:) start = Time.now.to_i yield if Time.now.to_i - start > 2 MyObservabilitySDK.notify("Operation #{name} (URL: #{request.url}) took more than 2 seconds") end end end ``` By adding `request:` to the method signature, the handler now receives the request object, which is one of the parameters the `controller.action.process` span provides. info Refer to the [API Documentation](https://api.rage-rb.dev/Rage/Telemetry/Spans) for a complete list of available spans and their parameters. #### Handling Errors When an observed operation fails with an exception, `yield` returns a [SpanResult](https://api.rage-rb.dev/Rage/Telemetry/SpanResult) object containing the error. This lets you track failures: app/telemetry/exception\_handler.rb ```ruby class ExceptionHandler < Rage::Telemetry::Handler handle "controller.action.process", with: :record_exceptions def self.record_exceptions result = yield MyObservabilitySDK.increment_errors if result.error? end end ``` The `SpanResult#error?` method returns `true` if an exception occurred, allowing you to track failed operations without interfering with normal exception handling. #### Span Matching Handlers can observe multiple spans by listing them explicitly: app/telemetry/cable\_handler.rb ```ruby class CableHandler < Rage::Telemetry::Handler handle "cable.connection.process", "cable.action.process", with: :monitor_duration def self.monitor_duration start = Time.now.to_i yield duration = Time.now.to_i - start MyObservabilitySDK.record_duration(duration) end end ``` ##### Using Wildcards You can also use wildcards to match multiple spans with a single pattern: app/telemetry/cable\_handler.rb ```ruby class CableHandler < Rage::Telemetry::Handler handle "cable.*", with: :monitor_duration def self.monitor_duration start = Time.now.to_i yield duration = Time.now.to_i - start MyObservabilitySDK.record_duration(duration) end end ``` warning When using wildcards, be aware that future framework versions may introduce new spans matching your pattern. If you need precise control over which spans to observe, prefer listing them explicitly. #### Integration Example Here's a complete example of integrating Rage's telemetry with a hypothetical metrics service to track operation durations: app/telemetry/metrics\_handler.rb ```ruby class MetricsHandler < Rage::Telemetry::Handler handle "controller.*", "cable.*", with: :track_duration def self.track_duration(name:) start = Process.clock_gettime(Process::CLOCK_MONOTONIC) result = yield duration = Process.clock_gettime(Process::CLOCK_MONOTONIC) - start # Send metrics to your observability platform MetricsService.record( metric: "#{name}.duration", value: duration, tags: { success: !result.error? } ) end end ``` Register the handler: config/application.rb ```ruby Rage.configure do config.telemetry.use MetricsHandler end ``` --- ### WebSockets `Rage::Cable` is Rage's WebSocket implementation, compatible with Action Cable. It enables real-time, bidirectional communication between your server and clients over a single WebSocket connection. #### Understanding Channels and Streams `Rage::Cable` uses two key concepts to route messages: * **Channels** - Client-side endpoints that clients subscribe to (e.g., `ChatChannel`, `NotificationsChannel`) * **Streams** - Server-side broadcast groups that clients know nothing about (e.g., `user_123_notifications`, `admin_data`) The distinction is important: clients subscribe to channels, but the server decides which streams to attach them to based on business logic like authentication and authorization. ##### Example: Dashboard Application Let's say you're building a dashboard that shows real-time data. Admins should see all data, while regular users see filtered data. Here's how channels and streams work together: 1. The client subscribes to the `DataChannel` 2. The server receives the subscription and checks the user's role: * If admin → attach to the `admin_data` stream * If regular user → attach to the `user_data` stream 3. When data updates occur, broadcast to the appropriate stream: ```ruby Rage::Cable.broadcast("admin_data", data) ``` Rage automatically forwards the message to all clients connected to that stream. info Refer to the API Documentation for a complete reference of `Rage::Cable` methods: * [Channel API](https://api.rage-rb.dev/Rage/Cable/Channel) * [Connection API](https://api.rage-rb.dev/Rage/Cable/Connection) #### Setup `Rage::Cable` runs as a separate Rack application, giving you flexibility to run it alongside your main app or as a standalone service in a separate process. ##### Mounting in `config.ru` Add this to your `config.ru` file to mount `Rage::Cable` at a specific path: config.ru ```ruby map "/cable" do run Rage::Cable.application end ``` You can also add WebSocket-specific middleware: config.ru ```ruby map "/cable" do use MyWebSocketRateLimiter run Rage::Cable.application end ``` ##### Mounting in Routes Alternatively, mount `Rage::Cable` directly in your routes file: config/routes.rb ```ruby Rage.routes.draw do mount Rage::Cable.application, at: "/cable" end ``` #### Connections Connections handle authentication when a client first connects to your WebSocket server. They let you accept or reject connections based on authentication credentials. app/channels/rage\_cable/connection.rb ```ruby module RageCable class Connection < Rage::Cable::Connection identified_by :current_user def connect self.current_user = find_verified_user end private def find_verified_user if verified_user = User.find_by(id: cookies.encrypted[:user_id]) verified_user else reject_unauthorized_connection end end end end ``` Here's what's happening: 1. **`identified_by :current_user`** - Defines an identifier for this connection. You can name it anything, but `current_user` or `current_account` are common choices. 2. **`connect` method** - Runs when a client connects. This is where you authenticate the connection and set the identifier. 3. **`reject_unauthorized_connection`** - Explicitly rejects the connection if authentication fails. warning Connections are **accepted by default**. You must explicitly call `reject_unauthorized_connection` to reject unauthorized clients. ##### Available Objects Inside the connection class, you have access to: * `request` - The HTTP request object * `cookies` - Cookie store for reading cookies * `session` - Session data * `params` - Subscription parameters #### Channels Channels are Ruby classes that handle specific types of real-time functionality in your application. Each channel represents a logical grouping of WebSocket functionality (like chat, notifications, or live updates). ##### Subscribing to Channels When a client subscribes to a channel, Rage calls the `subscribed` method. This is typically where you: 1. Verify authorization 2. Attach the connection to appropriate streams app/channels/data\_channel.rb ```ruby class DataChannel < Rage::Cable::Channel def subscribed if current_user.locked? reject return end if current_user.admin? stream_from "admin_data" else stream_from "user_data" end end end ``` You can call `reject` to refuse a subscription, though the WebSocket connection itself remains open for other channel subscriptions. ##### Broadcasting to Streams Use `broadcast` to send messages to all clients subscribed to a stream. This includes the client that triggered the broadcast: app/channels/chat\_channel.rb ```ruby class ChatChannel < Rage::Cable::Channel def subscribed stream_from "notifications" broadcast("notifications", { message: "A new member has joined!" }) end end ``` You can also broadcast from anywhere in your application (controllers, background jobs, etc.) using `Rage::Cable.broadcast`: ```ruby Rage::Cable.broadcast("notifications", { message: "A new member has joined!" }) ``` ##### Sending to Individual Connections Use `transmit` to send a message to only the current connection, bypassing streams entirely: app/channels/chat\_channel.rb ```ruby class ChatChannel < Rage::Cable::Channel def subscribed transmit({ message: "Welcome! You're now connected." }) end end ``` This is useful for sending personalized messages or acknowledgments to a specific client. ##### Receiving Messages Rage provides two ways to handle incoming messages from clients: ###### 1. Generic `receive` Method The `receive` method is called whenever a client sends any message to the channel: app/channels/chat\_channel.rb ```ruby class ChatChannel < Rage::Cable::Channel def receive(data) Message.create!(content: data["content"]) end end ``` warning The `data` parameter is always a hash with **string keys** (not symbols). ###### 2. RPC-Style Method Calls Clients can directly call any public method defined on your channel: app/channels/chat\_channel.rb ```ruby class ChatChannel < Rage::Cable::Channel def mark_as_read(data) Message.update!(data["id"], read: true) end def mark_as_unread(data) Message.update!(data["id"], read: false) end end ``` This RPC approach makes your channels feel like remote APIs, allowing clients to invoke specific actions. info RPC-style method calls are **not available** with the Raw JSON protocol. Use the generic `receive` method instead. #### Client-Side Integration ##### Action Cable Protocol With the default Action Cable protocol, use the [@rails/actioncable](https://www.npmjs.com/package/@rails/actioncable) JavaScript library: ```javascript import { createConsumer } from "@rails/actioncable" // Connect to the WebSocket server const cable = createConsumer("ws://localhost:3000/cable") // Subscribe to a channel const channel = cable.subscriptions.create("ChatChannel", { connected: () => console.log("connected"), received: (data) => console.log("received", data), }) // Send a message (triggers the `receive` method on the server) channel.send({ message: "Hello!" }) // Call RPC-style methods channel.perform("mark_as_read", { id: 123 }) channel.perform("mark_as_unread", { id: 456 }) ``` Notice there are no explicit routes - clients subscribe to channels by name, and Rage routes the messages accordingly. ##### Raw JSON Protocol If you prefer not to use the `@rails/actioncable` library, Rage supports a simpler Raw JSON protocol using the native browser WebSocket API: ```javascript // Connect directly to a channel const socket = new WebSocket("ws://localhost:3000/cable/chat") // Send messages as JSON socket.send(JSON.stringify({ message: "Hello!" })) ``` With Raw JSON protocol: * Each WebSocket connection maps to a single channel * Clients are automatically subscribed when they connect * No need for external dependencies Use the `config.cable.protocol` configuration to enable the Raw JSON protocol: ```ruby Rage.configure do config.cable.protocol = :raw_websocket_json end ``` info RPC-style method calls are **not supported** with the Raw JSON protocol. Use the generic `receive` method in your channel instead. #### Single-Server, Multi-Worker Setup If you run a single server with multiple Rage worker processes, you do not need an external backend (such as Redis) for `Rage::Cable` synchronization. Rage workers communicate with each other via IPC, so broadcasts are synchronized between workers on the same server. #### Multi-Server Setup with Redis When running `Rage::Cable` across multiple servers, you need a way to synchronize broadcasts between them. The Redis adapter solves this problem. ##### How It Works * Uses Redis Streams for reliable message delivery * Messages aren't lost during brief network disruptions * Only synchronizes messages **between servers** - if Redis goes down, clients on the same server as the broadcaster still receive messages ##### Configuration Create a `config/pubsub.yml` file with environment-specific settings: config/pubsub.yml ```yaml development: adapter: redis url: redis://localhost:6379/1 production: adapter: redis url: <%= ENV["REDIS_URL"] %> ``` Rage automatically loads this configuration and uses Redis to coordinate broadcasts across your server fleet. All keys except `adapter` and `channel_prefix` are passed directly to [redis-client](https://github.com/redis-rb/redis-client?tab=readme-ov-file#configuration). #### Benchmarks The following benchmark shows the ability of both Rage and Rails to handle 10000 concurrent WebSocket connections. **Test Environment:** * AWS EC2 `m5.large` instance * Ruby 3.3 * The test is running for five minutes **Client application:** * Opens 10,000 connections * Once a connection has been established, it subscribes to the `Chat` channel and starts sending messages at random intervals between 500ms and 2s * Additionally, once all connections are established, the client starts sending a broadcast message every second **Server application:** * Running two worker processes * Once a connection is accepted, the application subscribes it to the `chat_Best Room` stream * Once a regular message comes in, the server responds with the current timestamp * Once the broadcast message comes in, the server broadcasts the current timestamp to the `chat_Best Room` stream Source Code ```ruby class ChatChannel < Rage::Cable::Channel def subscribed stream_from "chat_#{params[:room]}" end def receive(data) transmit({ i: (Time.now.to_f * 1000).to_i }) end def broadcast_me broadcast("chat_#{params[:room]}", { i: (Time.now.to_f * 1000).to_i }) end end ``` **Results:** ---