<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Ken C. Demanawa</title>
    <description>The latest articles on DEV Community by Ken C. Demanawa (@kenneth_demanawa_fcc6581e).</description>
    <link>https://dev.to/kenneth_demanawa_fcc6581e</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F1646782%2F943f2b66-b578-402b-895d-b5785e6bf3a3.png</url>
      <title>DEV Community: Ken C. Demanawa</title>
      <link>https://dev.to/kenneth_demanawa_fcc6581e</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/kenneth_demanawa_fcc6581e"/>
    <language>en</language>
    <item>
      <title>Redis Isn't PostgreSQL: Building a Hybrid Change Data Capture Runtime in Ruby</title>
      <dc:creator>Ken C. Demanawa</dc:creator>
      <pubDate>Sat, 27 Jun 2026 02:26:37 +0000</pubDate>
      <link>https://dev.to/kenneth_demanawa_fcc6581e/redis-isnt-postgresql-building-a-hybrid-change-data-capture-runtime-in-ruby-49n5</link>
      <guid>https://dev.to/kenneth_demanawa_fcc6581e/redis-isnt-postgresql-building-a-hybrid-change-data-capture-runtime-in-ruby-49n5</guid>
      <description>&lt;h2&gt;
  
  
  I Built Commercial Redis CDC Source Drivers for Ruby — Here's What I Learned
&lt;/h2&gt;

&lt;p&gt;For the past couple of years I've been building a Change Data Capture (CDC) ecosystem for Ruby.&lt;/p&gt;

&lt;p&gt;Like many CDC projects, it started with PostgreSQL. PostgreSQL's Write-Ahead Log (WAL) is an excellent source of truth: durable, ordered, replayable, and well understood. It provides exactly the properties you want when you're building reliable event pipelines.&lt;/p&gt;

&lt;p&gt;But the deeper I went into distributed systems, the more I realized something important.&lt;/p&gt;

&lt;p&gt;Many systems don't observe change from PostgreSQL first.&lt;/p&gt;

&lt;p&gt;They observe it from Redis.&lt;/p&gt;

&lt;p&gt;Redis often sits at the front of modern architectures:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Redis Streams carry application events.&lt;/li&gt;
&lt;li&gt;Pub/Sub distributes transient state changes.&lt;/li&gt;
&lt;li&gt;Keyspace notifications react to cache invalidation and key expiry.&lt;/li&gt;
&lt;li&gt;Redis Cluster routes events across multiple primaries.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In many systems, Redis sees a change before PostgreSQL ever commits it.&lt;/p&gt;

&lt;p&gt;That raised an interesting question:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Can Redis become a first-class Change Data Capture source?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The obvious answer is "yes."&lt;/p&gt;

&lt;p&gt;The interesting answer is "yes—but not in the same way PostgreSQL does."&lt;/p&gt;

&lt;p&gt;That distinction eventually became &lt;strong&gt;cdc-redis-pro&lt;/strong&gt;, a commercial Redis source driver for the Ruby CDC ecosystem.&lt;/p&gt;

&lt;p&gt;This article isn't a product announcement.&lt;/p&gt;

&lt;p&gt;It's an engineering write-up about the architectural decisions behind the project, the tradeoffs Redis forces you to make, and the execution model that ultimately emerged.&lt;/p&gt;




&lt;h2&gt;
  
  
  Redis Doesn't Have One CDC Interface
&lt;/h2&gt;

&lt;p&gt;One misconception I frequently encounter is the assumption that Redis has an equivalent of PostgreSQL's WAL.&lt;/p&gt;

&lt;p&gt;It doesn't.&lt;/p&gt;

&lt;p&gt;Instead, Redis exposes several completely different mechanisms for observing change.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Source&lt;/th&gt;
&lt;th&gt;Delivery&lt;/th&gt;
&lt;th&gt;Replay&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Streams&lt;/td&gt;
&lt;td&gt;At-least-once&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Pub/Sub&lt;/td&gt;
&lt;td&gt;At-most-once&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Sharded Pub/Sub&lt;/td&gt;
&lt;td&gt;At-most-once&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Keyspace Notifications&lt;/td&gt;
&lt;td&gt;At-most-once&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;At first glance they all look like "events."&lt;/p&gt;

&lt;p&gt;Operationally they're completely different systems.&lt;/p&gt;

&lt;p&gt;Streams are durable.&lt;/p&gt;

&lt;p&gt;Pub/Sub isn't.&lt;/p&gt;

&lt;p&gt;Keyspace notifications exist primarily as operational signals.&lt;/p&gt;

&lt;p&gt;Sharded Pub/Sub introduces routing constraints that don't exist elsewhere.&lt;/p&gt;

&lt;p&gt;Treating them all as the same abstraction inevitably hides important guarantees—and hidden guarantees eventually become production incidents.&lt;/p&gt;

&lt;p&gt;Instead of pretending every Redis source behaves identically, I wanted the API to expose those differences explicitly.&lt;/p&gt;

&lt;p&gt;If a source cannot replay missed messages, the API should say so.&lt;/p&gt;

&lt;p&gt;If a reconnect creates a loss window, operators should know exactly when it happened.&lt;/p&gt;

&lt;p&gt;Infrastructure software shouldn't hide reality.&lt;/p&gt;

&lt;p&gt;It should make reality easier to reason about.&lt;/p&gt;




&lt;h2&gt;
  
  
  Redis and PostgreSQL Solve Different Problems
&lt;/h2&gt;

&lt;p&gt;A common question is:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"If Redis can generate change events, why not replace PostgreSQL CDC entirely?"&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Because they solve different problems.&lt;/p&gt;

&lt;p&gt;PostgreSQL's WAL is the durable history of your system.&lt;/p&gt;

&lt;p&gt;Redis is often the earliest signal that something is happening.&lt;/p&gt;

&lt;p&gt;One tells you &lt;strong&gt;what committed&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The other tells you &lt;strong&gt;what is happening right now&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;They're complementary.&lt;/p&gt;

&lt;p&gt;Not competing.&lt;/p&gt;

&lt;p&gt;Conceptually, I think about them like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;                    PostgreSQL WAL
                          │
                          ▼
                 Durable Record of Truth
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Redis Streams / PubSub / Keyspace
              │
              ▼
        Fast Operational Signal
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The goal isn't choosing one over the other.&lt;/p&gt;

&lt;p&gt;The goal is allowing both to participate in the same downstream processing pipeline.&lt;/p&gt;

&lt;p&gt;That required another architectural boundary.&lt;/p&gt;




&lt;h2&gt;
  
  
  A Common Language for Change Events
&lt;/h2&gt;

&lt;p&gt;One of the design goals of the broader CDC ecosystem is that downstream processors shouldn't care where an event originated.&lt;/p&gt;

&lt;p&gt;Whether a change comes from PostgreSQL logical replication or Redis Streams, the downstream processing model should remain identical.&lt;/p&gt;

&lt;p&gt;That boundary is &lt;code&gt;CDC::Core::ChangeEvent&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Instead of exposing PostgreSQL-specific or Redis-specific payloads to processors, each source is normalized into a common event model.&lt;/p&gt;

&lt;p&gt;Conceptually the pipeline looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;                PostgreSQL WAL
                     │                     
                pgoutput-client
                     │
                     ▼
                 ChangeEvent
                       ▲
                       │
                 cdc-redis-pro
                       │
        Streams / PubSub / Keyspace
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Everything downstream consumes the same normalized event.&lt;/p&gt;

&lt;p&gt;A webhook processor doesn't need to know whether the event came from WAL or Redis.&lt;/p&gt;

&lt;p&gt;A search indexing pipeline doesn't care.&lt;/p&gt;

&lt;p&gt;An audit sink doesn't care.&lt;/p&gt;

&lt;p&gt;Even the execution runtime doesn't care.&lt;/p&gt;

&lt;p&gt;That separation between &lt;strong&gt;source acquisition&lt;/strong&gt; and &lt;strong&gt;event processing&lt;/strong&gt; became one of the defining architectural decisions of the ecosystem.&lt;/p&gt;

&lt;p&gt;As the project grew, it became clear that acquiring events efficiently and processing them efficiently are two different problems—and they scale independently.&lt;/p&gt;

&lt;p&gt;That realization eventually led to a separate execution engine: &lt;strong&gt;cdc-orchestrator-pro&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;We'll come back to that shortly.&lt;/p&gt;

&lt;p&gt;First, let's look at what makes each Redis source fundamentally different.&lt;/p&gt;

&lt;h2&gt;
  
  
  Redis Isn't One Event System. It's Four.
&lt;/h2&gt;

&lt;p&gt;The first surprise when building a Redis CDC source is that there isn't a single Redis change stream.&lt;/p&gt;

&lt;p&gt;There are four.&lt;/p&gt;

&lt;p&gt;Each has different delivery guarantees.&lt;/p&gt;

&lt;p&gt;Each behaves differently during failures.&lt;/p&gt;

&lt;p&gt;Each recovers differently after reconnects.&lt;/p&gt;

&lt;p&gt;And each answers a different operational question.&lt;/p&gt;

&lt;p&gt;Treating them as interchangeable would have made the implementation simpler—but it also would have hidden the exact information operators need during production incidents.&lt;/p&gt;

&lt;p&gt;Instead, &lt;code&gt;cdc-redis-pro&lt;/code&gt; embraces those differences.&lt;/p&gt;




&lt;h2&gt;
  
  
  Redis Streams: The Durable Path
&lt;/h2&gt;

&lt;p&gt;Redis Streams is the closest thing Redis has to a traditional CDC source.&lt;/p&gt;

&lt;p&gt;Messages are persisted.&lt;/p&gt;

&lt;p&gt;Consumers maintain checkpoints.&lt;/p&gt;

&lt;p&gt;Consumer groups coordinate work.&lt;/p&gt;

&lt;p&gt;Failed consumers leave pending entries behind for recovery.&lt;/p&gt;

&lt;p&gt;In many ways, Streams feels familiar to anyone coming from Kafka or PostgreSQL logical replication.&lt;/p&gt;

&lt;p&gt;That made it the natural foundation for the recoverable side of the driver.&lt;/p&gt;

&lt;p&gt;The Streams implementation supports:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;XREAD&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;XREADGROUP&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Consumer Groups&lt;/li&gt;
&lt;li&gt;Pending-entry inspection&lt;/li&gt;
&lt;li&gt;&lt;code&gt;XAUTOCLAIM&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Duplicate suppression&lt;/li&gt;
&lt;li&gt;Optional dead-letter streams&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Operationally, Streams is the only Redis source that provides genuine replay.&lt;/p&gt;

&lt;p&gt;If a downstream worker crashes halfway through a batch, processing resumes from the last committed checkpoint rather than silently dropping work.&lt;/p&gt;

&lt;p&gt;Conceptually, it looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
             Producer
                │
                ▼
          Redis Stream
                │
          Consumer Group
                │
                ▼
          cdc-redis-pro
                │
           ChangeEvent
                │
                ▼
         Downstream Runtime
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is the strongest consistency story Redis offers.&lt;/p&gt;

&lt;p&gt;It isn't PostgreSQL's WAL—but it isn't trying to be.&lt;/p&gt;

&lt;p&gt;It's a durable event log designed for application-level workflows.&lt;/p&gt;




&lt;h2&gt;
  
  
  Pub/Sub: Fast, But Ephemeral
&lt;/h2&gt;

&lt;p&gt;Pub/Sub solves a completely different problem.&lt;/p&gt;

&lt;p&gt;Messages exist only while subscribers are connected.&lt;/p&gt;

&lt;p&gt;Disconnect for five seconds.&lt;/p&gt;

&lt;p&gt;Those five seconds are gone forever.&lt;/p&gt;

&lt;p&gt;That isn't a bug.&lt;/p&gt;

&lt;p&gt;It's the contract.&lt;/p&gt;

&lt;p&gt;Many libraries attempt to hide this by automatically reconnecting.&lt;/p&gt;

&lt;p&gt;The problem is that reconnecting doesn't recover missed messages.&lt;/p&gt;

&lt;p&gt;It only resumes receiving future ones.&lt;/p&gt;

&lt;p&gt;Pretending otherwise creates false confidence.&lt;/p&gt;

&lt;p&gt;Instead, &lt;code&gt;cdc-redis-pro&lt;/code&gt; treats Pub/Sub as an explicitly &lt;strong&gt;at-most-once&lt;/strong&gt; source.&lt;/p&gt;

&lt;p&gt;Reconnects are measured.&lt;/p&gt;

&lt;p&gt;Loss windows are reported.&lt;/p&gt;

&lt;p&gt;Operators can immediately see:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;when the disconnect occurred,&lt;/li&gt;
&lt;li&gt;how long the subscriber was offline,&lt;/li&gt;
&lt;li&gt;and exactly where message loss became possible.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That distinction matters.&lt;/p&gt;

&lt;p&gt;Infrastructure software shouldn't promise guarantees the underlying system doesn't provide.&lt;/p&gt;




&lt;h2&gt;
  
  
  Sharded Pub/Sub Changes the Topology
&lt;/h2&gt;

&lt;p&gt;Redis Cluster introduces another variation.&lt;/p&gt;

&lt;p&gt;Sharded Pub/Sub distributes channels across multiple primaries.&lt;/p&gt;

&lt;p&gt;That improves scalability, but it also means subscriptions become topology-aware.&lt;/p&gt;

&lt;p&gt;A reconnect isn't always reconnecting to the same node.&lt;/p&gt;

&lt;p&gt;During resharding, ownership of a channel may move entirely.&lt;/p&gt;

&lt;p&gt;Handling that correctly requires continuously tracking cluster topology rather than assuming a fixed server layout.&lt;/p&gt;

&lt;p&gt;The driver automatically discovers topology through &lt;code&gt;CLUSTER SHARDS&lt;/code&gt; and transparently rebinds subscriptions as ownership changes.&lt;/p&gt;

&lt;p&gt;To downstream processors, events continue arriving normally.&lt;/p&gt;

&lt;p&gt;To operators, topology changes remain observable.&lt;/p&gt;




&lt;h2&gt;
  
  
  Keyspace Notifications Aren't Really CDC
&lt;/h2&gt;

&lt;p&gt;Keyspace notifications are probably the easiest Redis feature to misunderstand.&lt;/p&gt;

&lt;p&gt;They're incredibly useful.&lt;/p&gt;

&lt;p&gt;They're also incredibly easy to misuse.&lt;/p&gt;

&lt;p&gt;Keyspace notifications exist to announce that Redis itself performed an operation:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a key expired,&lt;/li&gt;
&lt;li&gt;a value changed,&lt;/li&gt;
&lt;li&gt;a key was deleted,&lt;/li&gt;
&lt;li&gt;a hash was updated.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;They're operational signals.&lt;/p&gt;

&lt;p&gt;They're not durable history.&lt;/p&gt;

&lt;p&gt;They're not replayable.&lt;/p&gt;

&lt;p&gt;And by the time you receive an expiration notification, the value may already be gone.&lt;/p&gt;

&lt;p&gt;That's simply how Redis works.&lt;/p&gt;

&lt;p&gt;Rather than pretending every notification contains complete information, the driver offers optional best-effort value enrichment whenever the value still exists.&lt;/p&gt;

&lt;p&gt;If it doesn't, the event still proceeds.&lt;/p&gt;

&lt;p&gt;The guarantee remains explicit.&lt;/p&gt;




&lt;h2&gt;
  
  
  Delivery Guarantees Should Stay Visible
&lt;/h2&gt;

&lt;p&gt;One design principle shaped almost every API in the project.&lt;/p&gt;

&lt;p&gt;I didn't want to normalize away delivery semantics.&lt;/p&gt;

&lt;p&gt;Instead, I wanted them to remain visible all the way to the operator.&lt;/p&gt;

&lt;p&gt;Think of it like a database transaction.&lt;/p&gt;

&lt;p&gt;You wouldn't want a library to silently convert an eventually-consistent operation into something that merely &lt;em&gt;looks&lt;/em&gt; transactional.&lt;/p&gt;

&lt;p&gt;The same idea applies here.&lt;/p&gt;

&lt;p&gt;Different Redis sources have different operational characteristics.&lt;/p&gt;

&lt;p&gt;The API should preserve them.&lt;/p&gt;

&lt;p&gt;That philosophy can be summarized like this:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Source&lt;/th&gt;
&lt;th&gt;Replay&lt;/th&gt;
&lt;th&gt;Delivery&lt;/th&gt;
&lt;th&gt;Typical Use&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Streams&lt;/td&gt;
&lt;td&gt;✓&lt;/td&gt;
&lt;td&gt;At-least-once&lt;/td&gt;
&lt;td&gt;Durable workflows&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Pub/Sub&lt;/td&gt;
&lt;td&gt;✗&lt;/td&gt;
&lt;td&gt;At-most-once&lt;/td&gt;
&lt;td&gt;Live events&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Sharded Pub/Sub&lt;/td&gt;
&lt;td&gt;✗&lt;/td&gt;
&lt;td&gt;At-most-once&lt;/td&gt;
&lt;td&gt;Cluster-scale broadcasts&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Keyspace Notifications&lt;/td&gt;
&lt;td&gt;✗&lt;/td&gt;
&lt;td&gt;At-most-once&lt;/td&gt;
&lt;td&gt;Operational signals&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;None of these are "better."&lt;/p&gt;

&lt;p&gt;They're simply optimized for different workloads.&lt;/p&gt;




&lt;h2&gt;
  
  
  Topology Matters More Than Features
&lt;/h2&gt;

&lt;p&gt;Supporting Redis isn't just about supporting commands.&lt;/p&gt;

&lt;p&gt;It's about supporting deployments.&lt;/p&gt;

&lt;p&gt;A surprising amount of complexity came not from Streams or Pub/Sub themselves, but from the environments they run in.&lt;/p&gt;

&lt;p&gt;The driver currently supports:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Standalone Redis&lt;/li&gt;
&lt;li&gt;Redis Sentinel&lt;/li&gt;
&lt;li&gt;Redis Cluster&lt;/li&gt;
&lt;li&gt;TLS&lt;/li&gt;
&lt;li&gt;ACL authentication&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Cluster support turned out to be particularly interesting.&lt;/p&gt;

&lt;p&gt;Streams must remain within a single hash slot.&lt;/p&gt;

&lt;p&gt;Cross-slot reads fail.&lt;/p&gt;

&lt;p&gt;Pub/Sub subscriptions migrate during resharding.&lt;/p&gt;

&lt;p&gt;Connections disappear during primary failover.&lt;/p&gt;

&lt;p&gt;Those aren't edge cases.&lt;/p&gt;

&lt;p&gt;They're normal operating conditions in production.&lt;/p&gt;

&lt;p&gt;Every supported topology is continuously exercised using Docker-based integration tests covering failover, node restarts, resharding, authentication, and TLS.&lt;/p&gt;

&lt;p&gt;I wanted the implementation to reflect how Redis is actually deployed—not just how it behaves on a laptop.&lt;/p&gt;




&lt;h2&gt;
  
  
  Acquiring Events Is Only Half the Problem
&lt;/h2&gt;

&lt;p&gt;By this point, the source layer was capable of reliably acquiring events from every major Redis deployment model.&lt;/p&gt;

&lt;p&gt;The next question became much harder.&lt;/p&gt;

&lt;p&gt;How do you process them efficiently?&lt;/p&gt;

&lt;p&gt;One worker?&lt;/p&gt;

&lt;p&gt;Ten workers?&lt;/p&gt;

&lt;p&gt;Hundreds?&lt;/p&gt;

&lt;p&gt;How do you preserve ordering where it's required while still exploiting modern Ruby's parallelism?&lt;/p&gt;

&lt;p&gt;It turned out that reading events from Redis wasn't the difficult part.&lt;/p&gt;

&lt;p&gt;Scheduling what happened &lt;em&gt;after&lt;/em&gt; they were read became the real engineering challenge.&lt;/p&gt;

&lt;p&gt;That challenge eventually became &lt;strong&gt;HybridRuntime&lt;/strong&gt;, the execution engine inside &lt;strong&gt;cdc-orchestrator-pro&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;And surprisingly, the solution wasn't built around threads.&lt;/p&gt;

&lt;p&gt;It was built around ownership.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Architecture I'm Most Proud Of
&lt;/h2&gt;

&lt;p&gt;Surprisingly, reading events from Redis wasn't the hardest part of the project.&lt;/p&gt;

&lt;p&gt;Scheduling what happened &lt;em&gt;after&lt;/em&gt; those events arrived was.&lt;/p&gt;

&lt;p&gt;Modern Ruby gives us two powerful concurrency primitives:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Ractors&lt;/strong&gt; for parallel CPU execution&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fibers&lt;/strong&gt; for concurrent I/O&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Most systems choose one.&lt;/p&gt;

&lt;p&gt;I wanted both.&lt;/p&gt;

&lt;p&gt;That eventually became &lt;strong&gt;HybridRuntime&lt;/strong&gt;, the execution engine inside &lt;code&gt;cdc-orchestrator-pro&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Its job isn't tied to Redis.&lt;/p&gt;

&lt;p&gt;Redis simply happened to be the workload that exposed the problem first.&lt;/p&gt;




&lt;h2&gt;
  
  
  Event Acquisition and Event Processing Are Different Problems
&lt;/h2&gt;

&lt;p&gt;One architectural realization changed the direction of the project.&lt;/p&gt;

&lt;p&gt;Reading events from a source and processing those events are two completely different concerns.&lt;/p&gt;

&lt;p&gt;They're limited by different bottlenecks.&lt;/p&gt;

&lt;p&gt;They scale independently.&lt;/p&gt;

&lt;p&gt;A PostgreSQL logical replication connection is fundamentally serial.&lt;/p&gt;

&lt;p&gt;A Redis Stream consumer is similarly constrained.&lt;/p&gt;

&lt;p&gt;But once an event has been acquired and normalized into a &lt;code&gt;CDC::Core::ChangeEvent&lt;/code&gt;, downstream processing becomes embarrassingly parallel.&lt;/p&gt;

&lt;p&gt;That naturally separates the pipeline into two halves.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;                    Source Layer
                         │
         PostgreSQL WAL / Redis Streams
                         │
                         ▼
                CDC::Core::ChangeEvent
                         │
                         ▼
                  Execution Layer
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Once an event reaches the execution layer, its origin no longer matters.&lt;/p&gt;

&lt;p&gt;Redis.&lt;/p&gt;

&lt;p&gt;PostgreSQL.&lt;/p&gt;

&lt;p&gt;A future Kafka adapter.&lt;/p&gt;

&lt;p&gt;A future S3 replay.&lt;/p&gt;

&lt;p&gt;The runtime simply processes &lt;code&gt;ChangeEvent&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;That separation turned out to be one of the most valuable architectural decisions in the ecosystem.&lt;/p&gt;




&lt;h2&gt;
  
  
  HybridRuntime
&lt;/h2&gt;

&lt;p&gt;HybridRuntime combines two existing execution engines from the CDC ecosystem.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;cdc-parallel&lt;/strong&gt; provides pools of prewarmed Ractors for true CPU parallelism.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;cdc-concurrent&lt;/strong&gt; provides asynchronous Fiber pools for overlapping I/O within each Ractor.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Together they form a nested execution model.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;                 HybridRuntime
                        │
        ┌───────────────┴───────────────┐
        ▼                               ▼
  Ractor Pool                    Ractor Pool
        │                               │
        ▼                               ▼
   Fiber Pool                     Fiber Pool
        │                               │
        ▼                               ▼
Redis Connections              Redis Connections
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The interesting observation is that parallelism and concurrency solve different problems.&lt;/p&gt;

&lt;p&gt;Ractors increase throughput by executing work simultaneously.&lt;/p&gt;

&lt;p&gt;Fibers increase throughput by avoiding idle time while waiting for I/O.&lt;/p&gt;

&lt;p&gt;The runtime deliberately uses both.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Inception Pool
&lt;/h2&gt;

&lt;p&gt;As the architecture evolved, I noticed something amusing.&lt;/p&gt;

&lt;p&gt;Every layer owned another pool.&lt;/p&gt;

&lt;p&gt;The runtime owns a pool of Ractors.&lt;/p&gt;

&lt;p&gt;Each Ractor owns a LocalResourcePool.&lt;/p&gt;

&lt;p&gt;Each LocalResourcePool owns a pool of Fibers.&lt;/p&gt;

&lt;p&gt;Each Fiber owns a live Redis connection.&lt;/p&gt;

&lt;p&gt;It looked like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;HybridRuntime
     │
     ▼
Prewarmed Ractor Pool
     │
     ▼
LocalResourcePool
     │
     ▼
Fiber Pool
     │
     ▼
Redis Connections
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Internally I started calling it the &lt;strong&gt;Inception Pool&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;A pool containing pools containing pools.&lt;/p&gt;

&lt;p&gt;The name stuck.&lt;/p&gt;




&lt;h2&gt;
  
  
  Ownership Instead of Synchronization
&lt;/h2&gt;

&lt;p&gt;Most concurrent systems solve shared state by protecting it.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Threads
  │
  ▼
Mutex
  │
  ▼
Shared Connection Pool
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The more workers you add, the more frequently they compete for the same resources.&lt;/p&gt;

&lt;p&gt;Locks become unavoidable.&lt;/p&gt;

&lt;p&gt;HybridRuntime takes a different approach.&lt;/p&gt;

&lt;p&gt;Instead of synchronizing ownership...&lt;/p&gt;

&lt;p&gt;...it avoids sharing ownership entirely.&lt;/p&gt;

&lt;p&gt;Every Redis client is created inside the Ractor that will use it.&lt;/p&gt;

&lt;p&gt;It never leaves that Ractor.&lt;/p&gt;

&lt;p&gt;Nothing is borrowed.&lt;/p&gt;

&lt;p&gt;Nothing is shared.&lt;/p&gt;

&lt;p&gt;Nothing requires a mutex.&lt;/p&gt;

&lt;p&gt;Conceptually it looks like this.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Ractor 1
   │
   ├── Redis Connection A
   ├── Redis Connection B
   └── Fiber Scheduler

Ractor 2
   │
   ├── Redis Connection A
   ├── Redis Connection B
   └── Fiber Scheduler
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The only thing that crosses a Ractor boundary is an immutable &lt;code&gt;ChangeEvent&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Everything else remains local.&lt;/p&gt;

&lt;p&gt;This aligns naturally with Ruby's ownership model.&lt;/p&gt;

&lt;p&gt;Mutable state belongs somewhere.&lt;/p&gt;

&lt;p&gt;Rather than fighting that constraint, the runtime embraces it.&lt;/p&gt;




&lt;h1&gt;
  
  
  Why LocalResourcePool Exists
&lt;/h1&gt;

&lt;p&gt;That ownership model eventually led to another component:&lt;br&gt;
&lt;code&gt;CDC::Orchestrator::Pro::LocalResourcePool&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Unlike traditional connection pools, a &lt;code&gt;LocalResourcePool&lt;/code&gt; isn't shared across Ractors.&lt;/p&gt;

&lt;p&gt;The pool itself is shared as an immutable coordinator.&lt;/p&gt;

&lt;p&gt;The live resources are not.&lt;/p&gt;

&lt;p&gt;Instead, every Ractor lazily creates and owns its own resource pool the first time it needs one.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;             LocalResourcePool
                    │
      ┌─────────────┴─────────────┐
      ▼                           ▼
  Ractor A                   Ractor B
      │                           │
 Resource Pool               Resource Pool
      │                           │
 Redis Connections          Redis Connections
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each Ractor owns its resources for their entire lifetime.&lt;/p&gt;

&lt;p&gt;Nothing crosses a Ractor boundary.&lt;/p&gt;

&lt;p&gt;Nothing requires synchronization.&lt;/p&gt;

&lt;p&gt;The work moves.&lt;/p&gt;

&lt;p&gt;The connections don't.&lt;/p&gt;

&lt;p&gt;This turns out to be a natural fit for long-lived resources such as:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Redis clients&lt;/li&gt;
&lt;li&gt;PostgreSQL connections&lt;/li&gt;
&lt;li&gt;HTTP clients&lt;/li&gt;
&lt;li&gt;Elasticsearch clients&lt;/li&gt;
&lt;li&gt;S3 clients&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Every Ractor operates independently using resources it owns locally.&lt;/p&gt;

&lt;p&gt;Rather than coordinating access to a shared pool, the runtime coordinates immutable &lt;code&gt;ChangeEvent&lt;/code&gt;s while leaving the underlying resources exactly where they were created.&lt;/p&gt;

&lt;p&gt;The result is a simpler ownership model, reduced contention, and an execution architecture that scales naturally with additional Ractors.&lt;/p&gt;




&lt;h2&gt;
  
  
  Two Independent Scaling Axes
&lt;/h2&gt;

&lt;p&gt;Another consequence of this architecture is that acquisition and processing no longer have to scale together.&lt;/p&gt;

&lt;p&gt;Suppose a Redis deployment only needs three acquisition workers.&lt;/p&gt;

&lt;p&gt;That says nothing about how many processing workers you need.&lt;/p&gt;

&lt;p&gt;You might run:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Acquisition

3 Ractors
5 Fibers each

↓

Processing

7 Ractors
20 Fibers each
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each side can be tuned independently.&lt;/p&gt;

&lt;p&gt;Adding more downstream workers doesn't require opening additional Redis Streams.&lt;/p&gt;

&lt;p&gt;Adding more source readers doesn't require changing the execution topology.&lt;/p&gt;

&lt;p&gt;The two halves of the pipeline evolve independently.&lt;/p&gt;

&lt;p&gt;That separation proved invaluable during benchmarking because it exposed where the real bottlenecks actually lived.&lt;/p&gt;




&lt;h2&gt;
  
  
  Beyond Redis
&lt;/h2&gt;

&lt;p&gt;One realization surprised me.&lt;/p&gt;

&lt;p&gt;HybridRuntime wasn't solving a Redis problem.&lt;/p&gt;

&lt;p&gt;It was solving an event-processing problem.&lt;/p&gt;

&lt;p&gt;Redis happened to be the first source.&lt;/p&gt;

&lt;p&gt;The same execution model works for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;PostgreSQL logical replication&lt;/li&gt;
&lt;li&gt;Redis Streams&lt;/li&gt;
&lt;li&gt;Webhook delivery&lt;/li&gt;
&lt;li&gt;Search indexing&lt;/li&gt;
&lt;li&gt;Object storage sinks&lt;/li&gt;
&lt;li&gt;Future Kafka adapters&lt;/li&gt;
&lt;li&gt;Future message brokers&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Anything capable of producing a &lt;code&gt;CDC::Core::ChangeEvent&lt;/code&gt; automatically inherits the same execution engine.&lt;/p&gt;

&lt;p&gt;That ultimately justified extracting the runtime into its own commercial component: &lt;code&gt;cdc-orchestrator-pro&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Originally it lived inside another project.&lt;/p&gt;

&lt;p&gt;Eventually it became obvious that it wasn't a Redis runtime.&lt;/p&gt;

&lt;p&gt;It wasn't a Sidekiq runtime.&lt;/p&gt;

&lt;p&gt;It wasn't even a PostgreSQL runtime.&lt;/p&gt;

&lt;p&gt;It was an execution fabric for normalized change events.&lt;/p&gt;

&lt;p&gt;Redis simply happened to be the benchmark that inspired it.&lt;/p&gt;




&lt;h2&gt;
  
  
  Parallelism Isn't Free
&lt;/h2&gt;

&lt;p&gt;One thing the benchmarks made very clear is that parallelism isn't magic.&lt;/p&gt;

&lt;p&gt;Adding more Ractors doesn't produce linear speedups.&lt;/p&gt;

&lt;p&gt;It introduces coordination costs.&lt;/p&gt;

&lt;p&gt;Partition routing.&lt;/p&gt;

&lt;p&gt;Mailbox communication.&lt;/p&gt;

&lt;p&gt;Ordering constraints.&lt;/p&gt;

&lt;p&gt;Preserving correctness means accepting those costs.&lt;/p&gt;

&lt;p&gt;Understanding where those tradeoffs appear became just as interesting as the throughput numbers themselves.&lt;/p&gt;

&lt;p&gt;Let's look at what those benchmarks actually measured.&lt;/p&gt;




&lt;h2&gt;
  
  
  Where This Actually Fits
&lt;/h2&gt;

&lt;p&gt;After spending so much time discussing architecture, it's worth asking a simple question.&lt;/p&gt;

&lt;p&gt;Who actually needs this?&lt;/p&gt;

&lt;p&gt;The honest answer is:&lt;/p&gt;

&lt;p&gt;Not every Rails application.&lt;/p&gt;

&lt;p&gt;If Redis is simply a cache sitting beside your database, this project is probably unnecessary.&lt;/p&gt;

&lt;p&gt;Likewise, if every important state transition already commits to PostgreSQL before anything else happens, PostgreSQL logical replication alone may be all the CDC infrastructure you need.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;cdc-redis-pro&lt;/code&gt; exists for a much narrower class of systems.&lt;/p&gt;

&lt;p&gt;Systems where Redis is part of the application's event architecture rather than merely its cache.&lt;/p&gt;




&lt;h2&gt;
  
  
  Redis Streams as an Event Bus
&lt;/h2&gt;

&lt;p&gt;This is probably the most natural fit.&lt;/p&gt;

&lt;p&gt;Many distributed systems already use Redis Streams as their internal event bus.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Order Service
    │
    ▼
Redis Stream
    │
    ▼
Consumers
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Once Redis becomes the place where work is coordinated, durability suddenly matters.&lt;/p&gt;

&lt;p&gt;Consumers crash.&lt;/p&gt;

&lt;p&gt;Deployments restart.&lt;/p&gt;

&lt;p&gt;Networks partition.&lt;/p&gt;

&lt;p&gt;A consumer needs to know where to resume.&lt;/p&gt;

&lt;p&gt;Redis Streams already provides those building blocks.&lt;/p&gt;

&lt;p&gt;Consumer Groups.&lt;/p&gt;

&lt;p&gt;Pending Entries.&lt;/p&gt;

&lt;p&gt;Checkpoint IDs.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;XAUTOCLAIM&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The job of &lt;code&gt;cdc-redis-pro&lt;/code&gt; isn't replacing those mechanisms.&lt;/p&gt;

&lt;p&gt;It's integrating them into a larger event-processing pipeline while preserving their semantics.&lt;/p&gt;




&lt;h2&gt;
  
  
  Fast Signals Before Durable State
&lt;/h2&gt;

&lt;p&gt;Many systems generate transient events before anything reaches PostgreSQL.&lt;/p&gt;

&lt;p&gt;Examples include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;inventory availability&lt;/li&gt;
&lt;li&gt;market data&lt;/li&gt;
&lt;li&gt;IoT telemetry&lt;/li&gt;
&lt;li&gt;collaborative editing&lt;/li&gt;
&lt;li&gt;multiplayer game state&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These events often exist for milliseconds.&lt;/p&gt;

&lt;p&gt;Some are never intended to become permanent records.&lt;/p&gt;

&lt;p&gt;Waiting for a database commit before reacting introduces unnecessary latency.&lt;/p&gt;

&lt;p&gt;Redis already has the signal.&lt;/p&gt;

&lt;p&gt;The application simply needs a reliable way to observe it.&lt;/p&gt;

&lt;p&gt;That's exactly where Redis becomes a valuable CDC source.&lt;/p&gt;

&lt;p&gt;Not because it replaces the database.&lt;/p&gt;

&lt;p&gt;Because it observes change sooner.&lt;/p&gt;




&lt;h2&gt;
  
  
  Redis and PostgreSQL Together
&lt;/h2&gt;

&lt;p&gt;The architecture becomes much more interesting when both sources exist simultaneously.&lt;/p&gt;

&lt;p&gt;Imagine an order-processing pipeline.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Customer clicks Buy
       │
       ▼
Redis Stream
       │
 Immediate downstream processing
        │
 PostgreSQL Transaction
        │
        ▼
 Logical Replication
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Redis carries the operational signal.&lt;/p&gt;

&lt;p&gt;PostgreSQL records the durable history.&lt;/p&gt;

&lt;p&gt;Eventually both become the same normalized object.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Redis Streams
        │
        ▼
   ChangeEvent
        ▲
        │
PostgreSQL WAL
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Once normalized, downstream processing becomes identical.&lt;/p&gt;

&lt;p&gt;That separation allows each technology to do what it does best.&lt;/p&gt;

&lt;p&gt;Redis optimizes for responsiveness.&lt;/p&gt;

&lt;p&gt;PostgreSQL optimizes for durability.&lt;/p&gt;

&lt;p&gt;Neither replaces the other.&lt;/p&gt;




&lt;h2&gt;
  
  
  Event Processing Shouldn't Care About the Source
&lt;/h2&gt;

&lt;p&gt;One of the design goals of the CDC ecosystem is that processors shouldn't know—or care—where an event originated.&lt;/p&gt;

&lt;p&gt;A webhook dispatcher shouldn't behave differently because the event came from Redis instead of PostgreSQL.&lt;/p&gt;

&lt;p&gt;Neither should:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;search indexing&lt;/li&gt;
&lt;li&gt;audit sinks&lt;/li&gt;
&lt;li&gt;analytics&lt;/li&gt;
&lt;li&gt;cache invalidation&lt;/li&gt;
&lt;li&gt;AI pipelines&lt;/li&gt;
&lt;li&gt;object storage&lt;/li&gt;
&lt;li&gt;future message brokers&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Every processor consumes exactly the same event model.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Redis
   │
   ▼
 ChangeEvent
       ▲
       │
 PostgreSQL
      │
      ▼
 Processor
        │
 ┌──────┼────────┬────────┬────────┐
 ▼      ▼        ▼        ▼        ▼

Webhook Search  Audit   Redis   Future...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That separation is what allows the runtime to remain completely source-agnostic.&lt;/p&gt;




&lt;h2&gt;
  
  
  Ordered Workloads
&lt;/h2&gt;

&lt;p&gt;Not every workload benefits equally from parallelism.&lt;/p&gt;

&lt;p&gt;Suppose an application updates customer balances.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;+100
-20
+15
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Processing those out of order would produce incorrect state.&lt;/p&gt;

&lt;p&gt;Ordering matters.&lt;/p&gt;

&lt;p&gt;Other workloads don't have that constraint.&lt;/p&gt;

&lt;p&gt;Search indexing.&lt;/p&gt;

&lt;p&gt;Webhook fan-out.&lt;/p&gt;

&lt;p&gt;Telemetry aggregation.&lt;/p&gt;

&lt;p&gt;Independent cache updates.&lt;/p&gt;

&lt;p&gt;Those can often execute concurrently.&lt;/p&gt;

&lt;p&gt;One of the runtime's responsibilities is recognizing that not every processor requires the same ordering guarantees.&lt;/p&gt;

&lt;p&gt;Correctness always comes first.&lt;/p&gt;

&lt;p&gt;Throughput comes second.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why Not Just Use Sidekiq?
&lt;/h2&gt;

&lt;p&gt;This is probably the question Ruby developers ask most often.&lt;/p&gt;

&lt;p&gt;After all, Sidekiq already provides a robust distributed job system.&lt;/p&gt;

&lt;p&gt;The answer is that jobs and change streams solve different scheduling problems.&lt;/p&gt;

&lt;p&gt;A job queue answers:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"What work should execute next?"&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;A CDC runtime answers:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"How should related events flow through the system while preserving their correctness?"&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Those are similar questions.&lt;/p&gt;

&lt;p&gt;They're not the same question.&lt;/p&gt;

&lt;p&gt;Jobs are independent.&lt;/p&gt;

&lt;p&gt;Change events frequently aren't.&lt;/p&gt;

&lt;p&gt;Ordering.&lt;/p&gt;

&lt;p&gt;Checkpoints.&lt;/p&gt;

&lt;p&gt;Replay.&lt;/p&gt;

&lt;p&gt;Transaction boundaries.&lt;/p&gt;

&lt;p&gt;Partition routing.&lt;/p&gt;

&lt;p&gt;Those become first-class concerns in CDC systems.&lt;/p&gt;

&lt;p&gt;Rather than replacing Sidekiq, the runtime sits at a different layer.&lt;/p&gt;

&lt;p&gt;Sidekiq remains an excellent execution engine for background jobs.&lt;/p&gt;

&lt;p&gt;HybridRuntime focuses on ordered event pipelines.&lt;/p&gt;

&lt;p&gt;The two complement one another rather than compete.&lt;/p&gt;




&lt;h2&gt;
  
  
  Lessons Learned
&lt;/h2&gt;

&lt;p&gt;Building &lt;code&gt;cdc-redis-pro&lt;/code&gt; changed how I think about event-driven systems.&lt;/p&gt;

&lt;p&gt;A few observations kept appearing throughout development.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Redis isn't PostgreSQL.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Trying to force Redis into a WAL-shaped abstraction usually hides important operational behavior.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Delivery guarantees matter more than APIs.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Two systems exposing similar methods may have completely different recovery characteristics.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Ownership scales better than synchronization.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Keeping mutable resources inside a single Ractor proved simpler than sharing them across many workers.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Acquisition and processing are independent problems.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The bottleneck for reading events is rarely the same bottleneck for processing them.&lt;/p&gt;

&lt;p&gt;Treating those concerns separately made both architectures significantly cleaner.&lt;/p&gt;

&lt;p&gt;Most importantly...&lt;/p&gt;

&lt;p&gt;Infrastructure shouldn't hide tradeoffs.&lt;/p&gt;

&lt;p&gt;It should make them explicit.&lt;/p&gt;

&lt;p&gt;That's the philosophy behind the entire project.&lt;/p&gt;

&lt;p&gt;The benchmark results ended up reflecting exactly those design decisions.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the Benchmarks Actually Mean
&lt;/h2&gt;

&lt;p&gt;Benchmark numbers are easy to misunderstand.&lt;/p&gt;

&lt;p&gt;They're also surprisingly easy to exaggerate.&lt;/p&gt;

&lt;p&gt;I wanted to avoid both.&lt;/p&gt;

&lt;p&gt;Rather than publishing a single headline number, I built a benchmark matrix that explored how the runtime behaves under different execution strategies.&lt;/p&gt;

&lt;p&gt;The goal wasn't to find the biggest number.&lt;/p&gt;

&lt;p&gt;The goal was to understand where the architecture stops scaling—and why.&lt;/p&gt;




&lt;h2&gt;
  
  
  Measuring Different Parts of the Pipeline
&lt;/h2&gt;

&lt;p&gt;Not every benchmark measures the same thing.&lt;/p&gt;

&lt;p&gt;Some benchmarks measure source acquisition.&lt;/p&gt;

&lt;p&gt;Others measure downstream execution.&lt;/p&gt;

&lt;p&gt;Others measure the orchestration layer itself.&lt;/p&gt;

&lt;p&gt;Treating those numbers as interchangeable would be misleading.&lt;/p&gt;

&lt;p&gt;I ended up thinking about the benchmarks as three different phases.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Redis Source
      │
      ▼
ChangeEvent Acquisition
      │
      ▼
HybridRuntime
      │
      ▼
Downstream Sink
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each phase has different bottlenecks.&lt;/p&gt;

&lt;p&gt;Acquisition is constrained by Redis.&lt;/p&gt;

&lt;p&gt;Processing is constrained by CPU, I/O latency, ordering requirements, and scheduling overhead.&lt;/p&gt;

&lt;p&gt;Understanding which phase you're measuring is more important than the final throughput number.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Synthetic Benchmark
&lt;/h2&gt;

&lt;p&gt;The largest number observed was approximately &lt;strong&gt;54,500 events per second&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;That's intentionally &lt;strong&gt;not&lt;/strong&gt; presented as an end-to-end Redis benchmark.&lt;/p&gt;

&lt;p&gt;It measures the execution capacity of the orchestration layer after events have already been acquired.&lt;/p&gt;

&lt;p&gt;In other words:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ChangeEvent
      │
      ▼
HybridRuntime
      │
      ▼
Processor
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This benchmark answers a very specific question:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"How quickly can the runtime schedule and execute already-available work?"&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That's useful.&lt;/p&gt;

&lt;p&gt;It just isn't the same as measuring an entire Redis pipeline.&lt;/p&gt;




&lt;h2&gt;
  
  
  End-to-End Pipelines
&lt;/h2&gt;

&lt;p&gt;Real systems spend time doing real work.&lt;/p&gt;

&lt;p&gt;Reading from Redis.&lt;/p&gt;

&lt;p&gt;Writing to PostgreSQL.&lt;/p&gt;

&lt;p&gt;Calling HTTP services.&lt;/p&gt;

&lt;p&gt;Updating search indexes.&lt;/p&gt;

&lt;p&gt;Those operations introduce latency that no scheduler can eliminate.&lt;/p&gt;

&lt;p&gt;When measured end-to-end, the results naturally become lower.&lt;/p&gt;

&lt;p&gt;Current peak observations include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Redis Streams → Runtime: approximately &lt;strong&gt;17,600 events/sec&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;PostgreSQL WAL → Redis: approximately &lt;strong&gt;20,000 events/sec&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Those numbers include actual I/O rather than isolated scheduling.&lt;/p&gt;

&lt;p&gt;Personally, I find them more interesting than the synthetic benchmark because they reflect complete pipelines.&lt;/p&gt;




&lt;h2&gt;
  
  
  Scaling Isn't Linear
&lt;/h2&gt;

&lt;p&gt;One result immediately stood out.&lt;/p&gt;

&lt;p&gt;Adding more Ractors did &lt;strong&gt;not&lt;/strong&gt; produce proportional speedups.&lt;/p&gt;

&lt;p&gt;That's exactly what I expected.&lt;/p&gt;

&lt;p&gt;Parallelism always introduces coordination costs.&lt;/p&gt;

&lt;p&gt;Events must be routed.&lt;/p&gt;

&lt;p&gt;Partitions must remain consistent.&lt;/p&gt;

&lt;p&gt;Workers communicate through Ractor mailboxes.&lt;/p&gt;

&lt;p&gt;Ordering constraints occasionally delay otherwise-complete work.&lt;/p&gt;

&lt;p&gt;The runtime spends part of its time doing useful work...&lt;/p&gt;

&lt;p&gt;...and part of its time coordinating that work.&lt;/p&gt;

&lt;p&gt;That coordination isn't overhead to eliminate.&lt;/p&gt;

&lt;p&gt;It's the cost of preserving correctness.&lt;/p&gt;

&lt;p&gt;The benchmark matrix made those tradeoffs visible.&lt;/p&gt;

&lt;p&gt;Rather than chasing perfect scaling, the goal became identifying the point where additional parallelism stopped producing meaningful throughput gains.&lt;/p&gt;

&lt;p&gt;For the current implementation, that sweet spot consistently appeared around:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;3 prewarmed Ractors&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;5 Redis connections per Ractor&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;50 Fibers&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That balance delivered high throughput without introducing excessive scheduling overhead.&lt;/p&gt;




&lt;h2&gt;
  
  
  Ordering Has a Cost
&lt;/h2&gt;

&lt;p&gt;One benchmark compared ordered and unordered execution.&lt;/p&gt;

&lt;p&gt;The difference wasn't dramatic.&lt;/p&gt;

&lt;p&gt;Ordered execution consistently performed slightly slower.&lt;/p&gt;

&lt;p&gt;That's expected.&lt;/p&gt;

&lt;p&gt;Maintaining ordering means the runtime occasionally waits for earlier work to complete before later work can safely continue.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Event 1
Event 2
Event 3
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;cannot become:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Event 2
Event 3
Event 1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;simply because Event 2 happened to finish first.&lt;/p&gt;

&lt;p&gt;Preserving correctness sometimes requires sacrificing a little throughput.&lt;/p&gt;

&lt;p&gt;That's a tradeoff I consider worthwhile.&lt;/p&gt;

&lt;p&gt;Correctness scales better than debugging race conditions.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Interesting Bottleneck
&lt;/h2&gt;

&lt;p&gt;The benchmark wasn't really about Redis.&lt;/p&gt;

&lt;p&gt;It was about coordination.&lt;/p&gt;

&lt;p&gt;At low parallelism, workers spend most of their time processing events.&lt;/p&gt;

&lt;p&gt;At high parallelism, workers spend increasingly more time coordinating with one another.&lt;/p&gt;

&lt;p&gt;Eventually another Ractor contributes more scheduling overhead than useful work.&lt;/p&gt;

&lt;p&gt;Finding that point was considerably more valuable than finding the largest throughput number.&lt;/p&gt;

&lt;p&gt;It answered a much more practical question:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"How should I actually configure this in production?"&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Chaos Matters More Than Throughput
&lt;/h2&gt;

&lt;p&gt;Raw throughput is only one characteristic of an event pipeline.&lt;/p&gt;

&lt;p&gt;Recovery behavior is arguably more important.&lt;/p&gt;

&lt;p&gt;The benchmark suite includes failure scenarios covering:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Redis restarts&lt;/li&gt;
&lt;li&gt;PostgreSQL restarts&lt;/li&gt;
&lt;li&gt;connection interruption&lt;/li&gt;
&lt;li&gt;checkpoint recovery&lt;/li&gt;
&lt;li&gt;consumer recovery&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Streams resumed processing from checkpoints.&lt;/p&gt;

&lt;p&gt;Pub/Sub sources reported explicit loss windows.&lt;/p&gt;

&lt;p&gt;Recovery behavior remained consistent with each source's documented guarantees.&lt;/p&gt;

&lt;p&gt;That consistency mattered more to me than achieving another few thousand events per second.&lt;/p&gt;




&lt;h2&gt;
  
  
  Long-Running Stability
&lt;/h2&gt;

&lt;p&gt;Short benchmarks rarely expose operational problems.&lt;/p&gt;

&lt;p&gt;Memory leaks.&lt;/p&gt;

&lt;p&gt;Connection exhaustion.&lt;/p&gt;

&lt;p&gt;Scheduler starvation.&lt;/p&gt;

&lt;p&gt;Queue growth.&lt;/p&gt;

&lt;p&gt;Those usually appear over time.&lt;/p&gt;

&lt;p&gt;The runtime was therefore exercised continuously using soak tests.&lt;/p&gt;

&lt;p&gt;One representative run processed approximately &lt;strong&gt;1.34 million events&lt;/strong&gt; over five minutes.&lt;/p&gt;

&lt;p&gt;No processing failures were observed.&lt;/p&gt;

&lt;p&gt;Median throughput degraded by roughly &lt;strong&gt;2%&lt;/strong&gt; over the duration of the run.&lt;/p&gt;

&lt;p&gt;That's encouraging, although much longer overnight and multi-day soak tests remain on my roadmap.&lt;/p&gt;

&lt;p&gt;Operational confidence comes from sustained behavior—not just impressive graphs.&lt;/p&gt;




&lt;h2&gt;
  
  
  What I Learned
&lt;/h2&gt;

&lt;p&gt;Perhaps the most surprising outcome of the benchmarking work was this:&lt;/p&gt;

&lt;p&gt;The execution runtime wasn't the limiting factor.&lt;/p&gt;

&lt;p&gt;The limiting factor was almost always the surrounding system.&lt;/p&gt;

&lt;p&gt;Network latency.&lt;/p&gt;

&lt;p&gt;Redis.&lt;/p&gt;

&lt;p&gt;HTTP endpoints.&lt;/p&gt;

&lt;p&gt;Disk.&lt;/p&gt;

&lt;p&gt;Database writes.&lt;/p&gt;

&lt;p&gt;The scheduler spent most of its time waiting for external systems.&lt;/p&gt;

&lt;p&gt;That reinforced one of the central architectural decisions behind HybridRuntime.&lt;/p&gt;

&lt;p&gt;Fibers overlap waiting.&lt;/p&gt;

&lt;p&gt;Ractors overlap computation.&lt;/p&gt;

&lt;p&gt;Neither attempts to eliminate latency.&lt;/p&gt;

&lt;p&gt;They simply ensure latency in one part of the system doesn't unnecessarily stall everything else.&lt;/p&gt;

&lt;p&gt;The result isn't infinite scalability.&lt;/p&gt;

&lt;p&gt;It's predictable scalability.&lt;/p&gt;

&lt;p&gt;And for infrastructure software, predictability is usually the more valuable property.&lt;/p&gt;




&lt;p&gt;The complete benchmark reports—including raw CSV data, SVG charts, chaos-recovery artifacts, and soak-test results—are published alongside the documentation.&lt;/p&gt;

&lt;p&gt;I'd much rather readers inspect the raw data than rely on a single headline number.&lt;/p&gt;

&lt;p&gt;Benchmarks are most useful when they're reproducible.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's Next
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;cdc-redis-pro&lt;/code&gt; is only one piece of a much larger ecosystem.&lt;/p&gt;

&lt;p&gt;The long-term goal was never to build "yet another Redis client."&lt;/p&gt;

&lt;p&gt;The goal was to build a source-agnostic Change Data Capture platform for Ruby.&lt;/p&gt;

&lt;p&gt;Today, PostgreSQL logical replication and Redis happen to be the two primary sources.&lt;/p&gt;

&lt;p&gt;Tomorrow, that could just as easily include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Kafka&lt;/li&gt;
&lt;li&gt;NATS&lt;/li&gt;
&lt;li&gt;Amazon SQS&lt;/li&gt;
&lt;li&gt;Webhooks&lt;/li&gt;
&lt;li&gt;Object storage&lt;/li&gt;
&lt;li&gt;Search indexes&lt;/li&gt;
&lt;li&gt;Other databases&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The important observation is that the runtime doesn't need to change.&lt;/p&gt;

&lt;p&gt;As long as a source can be normalized into a &lt;code&gt;CDC::Core::ChangeEvent&lt;/code&gt;, everything downstream already knows how to process it.&lt;/p&gt;

&lt;p&gt;That was the motivation behind separating source acquisition from execution.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;        Source
           │
           ▼
   CDC::Core::ChangeEvent
           │
           ▼
    cdc-orchestrator-pro
           │
           ▼
        Processors
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every new source becomes an adapter.&lt;/p&gt;

&lt;p&gt;Not a new runtime.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why Split the Runtime?
&lt;/h2&gt;

&lt;p&gt;One architectural decision deserves a brief explanation.&lt;/p&gt;

&lt;p&gt;Originally the execution engine lived inside another project.&lt;/p&gt;

&lt;p&gt;As the ecosystem evolved, I realized something important.&lt;/p&gt;

&lt;p&gt;The runtime wasn't solving a Redis problem.&lt;/p&gt;

&lt;p&gt;It wasn't solving a PostgreSQL problem.&lt;/p&gt;

&lt;p&gt;It wasn't even solving a Sidekiq problem.&lt;/p&gt;

&lt;p&gt;It was solving an event-processing problem.&lt;/p&gt;

&lt;p&gt;That realization led to extracting the execution engine into its own commercial component:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;cdc-orchestrator-pro&lt;/code&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Today it powers Redis CDC.&lt;/p&gt;

&lt;p&gt;Tomorrow it can power any source capable of producing normalized change events.&lt;/p&gt;

&lt;p&gt;Separating those concerns keeps both halves of the system simpler.&lt;/p&gt;

&lt;p&gt;Source adapters acquire events.&lt;/p&gt;

&lt;p&gt;HybridRuntime processes them.&lt;/p&gt;

&lt;p&gt;Each evolves independently.&lt;/p&gt;




&lt;h2&gt;
  
  
  Open Source First
&lt;/h2&gt;

&lt;p&gt;Although &lt;code&gt;cdc-redis-pro&lt;/code&gt; and &lt;code&gt;cdc-orchestrator-pro&lt;/code&gt; are commercial products, the ecosystem they're built upon remains open source.&lt;/p&gt;

&lt;p&gt;That includes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;cdc-core&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;cdc-parallel&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;cdc-concurrent&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;pgoutput-client&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;pgoutput-parser&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;pgoutput-decoder&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;pgoutput-source-adapter&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Mammoth&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Those projects define the common event model, execution primitives, and PostgreSQL integration that everything else builds upon.&lt;/p&gt;

&lt;p&gt;The commercial components focus on operational capabilities rather than replacing the open-source foundation.&lt;/p&gt;

&lt;p&gt;That separation is intentional.&lt;/p&gt;

&lt;p&gt;I believe infrastructure ecosystems become valuable through adoption and trust—not artificial feature restrictions.&lt;/p&gt;




&lt;h2&gt;
  
  
  Looking Ahead
&lt;/h2&gt;

&lt;p&gt;Redis replication remains one of the larger pieces still on the roadmap.&lt;/p&gt;

&lt;p&gt;Today, &lt;code&gt;cdc-redis-pro&lt;/code&gt; consumes Redis event sources such as Streams, Pub/Sub, and Keyspace Notifications.&lt;/p&gt;

&lt;p&gt;A future version will move further upstream by treating Redis itself as a replication source.&lt;/p&gt;

&lt;p&gt;That's a significantly more ambitious problem.&lt;/p&gt;

&lt;p&gt;I'd rather stabilize the current architecture before expanding its scope.&lt;/p&gt;

&lt;p&gt;There are also areas where I think the execution engine itself can continue to improve.&lt;/p&gt;

&lt;p&gt;Adaptive scheduling.&lt;/p&gt;

&lt;p&gt;Smarter partition routing.&lt;/p&gt;

&lt;p&gt;Better observability.&lt;/p&gt;

&lt;p&gt;Long-running soak tests.&lt;/p&gt;

&lt;p&gt;More topology-aware execution.&lt;/p&gt;

&lt;p&gt;Those improvements belong to the runtime rather than any particular source adapter—which is exactly why separating acquisition from execution turned out to be such a useful architectural boundary.&lt;/p&gt;




&lt;h2&gt;
  
  
  Final Thoughts
&lt;/h2&gt;

&lt;p&gt;I started this project thinking I was building Redis CDC.&lt;/p&gt;

&lt;p&gt;Somewhere along the way I realized I was really building an execution model.&lt;/p&gt;

&lt;p&gt;Redis happened to expose the problem first.&lt;/p&gt;

&lt;p&gt;PostgreSQL reinforced it.&lt;/p&gt;

&lt;p&gt;Future source adapters will probably validate it again.&lt;/p&gt;

&lt;p&gt;The most interesting lesson wasn't about Redis at all.&lt;/p&gt;

&lt;p&gt;It was this:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Acquiring events and processing events are different problems.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;They have different bottlenecks.&lt;/p&gt;

&lt;p&gt;They scale differently.&lt;/p&gt;

&lt;p&gt;They deserve different architectures.&lt;/p&gt;

&lt;p&gt;Once those responsibilities are separated, the rest of the system becomes remarkably composable.&lt;/p&gt;

&lt;p&gt;Redis becomes another source.&lt;/p&gt;

&lt;p&gt;PostgreSQL becomes another source.&lt;/p&gt;

&lt;p&gt;Tomorrow's adapters become just that—adapters.&lt;/p&gt;

&lt;p&gt;The runtime stays the same.&lt;/p&gt;

&lt;p&gt;For me, that's the most exciting part of the entire project.&lt;/p&gt;

&lt;p&gt;Not because it produced the largest benchmark numbers.&lt;/p&gt;

&lt;p&gt;Not because it uses Ractors or Fibers.&lt;/p&gt;

&lt;p&gt;But because it led to an architecture that's easier to reason about, easier to extend, and honest about the tradeoffs of the systems it builds upon.&lt;/p&gt;

&lt;p&gt;The benchmark reports are public.&lt;/p&gt;

&lt;p&gt;The documentation is public.&lt;/p&gt;

&lt;p&gt;The implementation is commercial.&lt;/p&gt;

&lt;p&gt;If you're building event-driven systems in Ruby—or you're wrestling with Redis and PostgreSQL in the same architecture—I'd genuinely love to hear how you're approaching those problems.&lt;/p&gt;

&lt;p&gt;I'm convinced there's still a lot left to explore.&lt;/p&gt;

</description>
      <category>redis</category>
      <category>ruby</category>
      <category>postgres</category>
      <category>architecture</category>
    </item>
    <item>
      <title>🛡️ RackJwtAegis: Rack Middleware for Multi-Tenant JWT Authentication (Part 1)</title>
      <dc:creator>Ken C. Demanawa</dc:creator>
      <pubDate>Thu, 14 Aug 2025 06:53:45 +0000</pubDate>
      <link>https://dev.to/kenneth_demanawa_fcc6581e/rackjwtaegis-rack-middleware-for-multi-tenant-jwt-authentication-part-1-34lg</link>
      <guid>https://dev.to/kenneth_demanawa_fcc6581e/rackjwtaegis-rack-middleware-for-multi-tenant-jwt-authentication-part-1-34lg</guid>
      <description>&lt;p&gt;&lt;em&gt;Implementing middleware-level JWT authentication and authorization before requests reach your Rails application&lt;/em&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;RackJwtAegis&lt;/strong&gt; is a Rack middleware that provides JWT authentication and authorization &lt;strong&gt;before requests reach your Rails or Rack application&lt;/strong&gt;. It validates JWT claims at the middleware layer, enabling &lt;strong&gt;multi&lt;/strong&gt;-tenancy and security without touching your application code. This is &lt;strong&gt;Part 1&lt;/strong&gt; of a 2-part series focusing on basic setup and multi-tenant configuration. &lt;strong&gt;Part 2&lt;/strong&gt; will cover advanced RBAC (Role-Based Access Control) and caching strategies.&lt;/p&gt;

&lt;p&gt;🔗 &lt;strong&gt;Links:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;RubyGems&lt;/strong&gt;: &lt;a href="https://rubygems.org/gems/rack_jwt_aegis" rel="noopener noreferrer"&gt;rack_jwt_aegis&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;GitHub&lt;/strong&gt;: &lt;a href="https://github.com/kanutocd/rack_jwt_aegis" rel="noopener noreferrer"&gt;RackJwtAegis Repository&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Demo App&lt;/strong&gt;: &lt;a href="https://github.com/kanutocd/rack-jwt-aegis-example" rel="noopener noreferrer"&gt;Multi-Tenant Demo (Proof-of-Concept)&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Why Another JWT Middleware?
&lt;/h2&gt;

&lt;p&gt;Most JWT gems handle basic authentication, but many applications need additional features:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Multi-tenant isolation&lt;/strong&gt; with header-based tenant id and request host subdomain validation&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Path-based authorization&lt;/strong&gt; using company slugs from JWT payloads&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Middleware-level validation&lt;/strong&gt; that occurs before requests reach your application&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Built-in caching strategies&lt;/strong&gt; for performance optimization&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Support for organizational hierarchies&lt;/strong&gt; without application code changes&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;RackJwtAegis was built to handle these scenarios at the &lt;strong&gt;Rack middleware layer&lt;/strong&gt;, intercepting and validating requests before they reach your Rails controllers.&lt;/p&gt;




&lt;h2&gt;
  
  
  🏗️ The Demo: Multi-Tenant Proof-of-Concept
&lt;/h2&gt;

&lt;p&gt;To demonstrate RackJwtAegis features, I built a &lt;strong&gt;basic proof-of-concept application&lt;/strong&gt; with a multi-tenant ERP structure:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;⚠️ Note&lt;/strong&gt;: This is an &lt;strong&gt;unfinished demo application&lt;/strong&gt; that only returns success messages - no actual business logic is implemented. It exists solely to showcase middleware authentication patterns.&lt;/p&gt;

&lt;h3&gt;
  
  
  Architecture Overview
&lt;/h3&gt;

&lt;p&gt;Company Group (Tenant)&lt;br&gt;
├── Company A (Manufacturing)&lt;br&gt;
├── Company B (Retail)&lt;br&gt;
├── Company C (Logistics)&lt;br&gt;
├── Company D (Technology)&lt;br&gt;
└── Company E (Wholesale)&lt;/p&gt;

&lt;p&gt;Each company has 7 ERP modules (demo structure only):&lt;br&gt;
📊 Accounting  📦 Inventory  🛒 Procurement  💼 Sales&lt;br&gt;
🏪 Retail     📍 Warehouse  🏭 Wholesale&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Note&lt;/strong&gt;: These are placeholder modules for demonstrating multi-tenant routing - no actual ERP features are implemented.&lt;/p&gt;
&lt;h3&gt;
  
  
  Demo Permission Matrix
&lt;/h3&gt;

&lt;p&gt;The application demonstrates these access patterns (without actual business logic):&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Super Admin&lt;/strong&gt;: Full access across all companies and modules&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Group CFO&lt;/strong&gt;: Financial access across multiple companies&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Company Admin&lt;/strong&gt;: Full access within their assigned companies&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Department Manager&lt;/strong&gt;: Module-specific access (e.g., Sales Manager)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Employee&lt;/strong&gt;: Limited access based on role and assignment&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Note&lt;/strong&gt;: All endpoints return simple success messages like &lt;code&gt;{"message": "Reports retrieved successfully"}&lt;/code&gt; to demonstrate authentication flow.&lt;/p&gt;


&lt;h2&gt;
  
  
  ⚡ Quick Setup &amp;amp; Configuration
&lt;/h2&gt;
&lt;h3&gt;
  
  
  1. Installation
&lt;/h3&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Gemfile&lt;/span&gt;
&lt;span class="n"&gt;gem&lt;/span&gt; &lt;span class="s1"&gt;'rack_jwt_aegis'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'~&amp;gt; 1.1.0'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;bundle &lt;span class="nb"&gt;install&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;h3&gt;
  
  
  2. Basic Configuration
&lt;/h3&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# config/application.rb&lt;/span&gt;
&lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;middleware&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;insert_before&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="no"&gt;RackJwtAegis&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Middleware&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="ss"&gt;jwt_secret: &lt;/span&gt;&lt;span class="no"&gt;ENV&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'JWT_SECRET'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="ss"&gt;skip_paths: &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'/api/v1/auth/login'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'/health'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;&lt;strong&gt;What this configuration enables:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;JWT Token Validation&lt;/strong&gt;: The middleware validates JWT signatures using your secret key before any request reaches your Rails API-only application&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Authentication Layer&lt;/strong&gt;: Automatically extracts and validates JWT tokens from the &lt;code&gt;Authorization: Bearer &amp;lt;token&amp;gt;&lt;/code&gt; header for API endpoints&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Public Endpoints&lt;/strong&gt;: The &lt;code&gt;skip_paths&lt;/code&gt; array allows specific API routes (like login and health checks) to bypass JWT validation&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Middleware Positioning&lt;/strong&gt;: &lt;code&gt;insert_before 0&lt;/code&gt; ensures JWT validation happens first, before any other middleware or Rails API routing&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;API-First Security&lt;/strong&gt;: Perfect for Rails API-only applications that serve mobile apps, SPAs, or microservices&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Result&lt;/strong&gt;: Your Rails API-only application now has middleware-level JWT authentication. Invalid tokens are rejected with HTTP 401 before reaching your API controllers.&lt;/p&gt;
&lt;h3&gt;
  
  
  3. Multi-Tenant Configuration
&lt;/h3&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# config/application.rb - Multi-tenant configuration&lt;/span&gt;
&lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;middleware&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;insert_before&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="no"&gt;RackJwtAegis&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Middleware&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="ss"&gt;jwt_secret: &lt;/span&gt;&lt;span class="no"&gt;ENV&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'JWT_SECRET'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="s1"&gt;'your-super-secret-jwt-key-for-development'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="ss"&gt;tenant_id_header_name: &lt;/span&gt;&lt;span class="s1"&gt;'X-Company-Group-Id'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="ss"&gt;validate_tenant_id: &lt;/span&gt;&lt;span class="kp"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="ss"&gt;validate_pathname_slug: &lt;/span&gt;&lt;span class="kp"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="ss"&gt;validate_subdomain: &lt;/span&gt;&lt;span class="kp"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="ss"&gt;skip_paths: &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'/api/v1/auth/login'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'/up'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'/api/v1/health'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="c1"&gt;# Custom JWT payload mapping for multi-tenant access control&lt;/span&gt;
  &lt;span class="ss"&gt;payload_mapping: &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="ss"&gt;user_id: :sub&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="ss"&gt;tenant_id: :company_group_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="ss"&gt;subdomain: :company_group_domain&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="ss"&gt;pathname_slugs: :company_slugs&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="ss"&gt;role_ids: :role_ids&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;&lt;strong&gt;What this advanced configuration enables:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Tenant Header Validation&lt;/strong&gt;: The &lt;code&gt;tenant_id_header_name&lt;/code&gt; setting requires API requests to include an &lt;code&gt;X-Company-Group-Id&lt;/code&gt; header that must match the &lt;code&gt;company_group_id&lt;/code&gt; in the JWT payload, preventing cross-tenant access&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Subdomain Security&lt;/strong&gt;: &lt;code&gt;validate_subdomain: true&lt;/code&gt; ensures the request subdomain (e.g., &lt;code&gt;acme-corp.localhost.local&lt;/code&gt;) matches the JWT's &lt;code&gt;company_group_domain&lt;/code&gt; field for API-only applications&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Path-Based Authorization&lt;/strong&gt;: &lt;code&gt;validate_pathname_slug: true&lt;/code&gt; validates that company slugs in API URLs (e.g., &lt;code&gt;/api/v1/acme-manufacturing/...&lt;/code&gt;) exist in the JWT's &lt;code&gt;company_slugs&lt;/code&gt; array&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Custom Payload Mapping&lt;/strong&gt;: The &lt;code&gt;payload_mapping&lt;/code&gt; hash tells the middleware where to find tenant isolation data in your JWT structure for API-only authentication&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Role Integration&lt;/strong&gt;: Maps &lt;code&gt;role_ids&lt;/code&gt; from JWT payload for downstream RBAC processing in API controllers&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Multi-Tenant API Security&lt;/strong&gt;: Ideal for Rails API-only applications serving multiple tenants through mobile apps, SPAs, or B2B integrations&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Result&lt;/strong&gt;: Three-layer middleware security validation (JWT → Tenant → Path) that enforces multi-tenant isolation before requests reach your Rails API-only application. Users can only access companies they're authorized for, and tenant isolation is guaranteed at the middleware level for all API endpoints.&lt;/p&gt;


&lt;h2&gt;
  
  
  🎯 Consuming Validated Data in Rails Controllers
&lt;/h2&gt;

&lt;p&gt;After RackJwtAegis middleware validates the JWT and enforces multi-tenant security, your Rails API controllers can access the validated user context through &lt;code&gt;RequestContext&lt;/code&gt;:&lt;/p&gt;
&lt;h3&gt;
  
  
  Basic Controller Usage
&lt;/h3&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# app/controllers/api/v1/accounting/reports_controller.rb&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Api::V1::Accounting::ReportsController&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;ApplicationController&lt;/span&gt;
  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;index&lt;/span&gt;
    &lt;span class="c1"&gt;# Check if request passed middleware authentication&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;unauthorized&lt;/span&gt; &lt;span class="k"&gt;unless&lt;/span&gt; &lt;span class="no"&gt;RackJwtAegis&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;RequestContext&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;authenticated?&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;env&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c1"&gt;# Get validated user information from middleware&lt;/span&gt;
    &lt;span class="n"&gt;user_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;RackJwtAegis&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;RequestContext&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;env&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;tenant_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;RackJwtAegis&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;RequestContext&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;tenant_id&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;env&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c1"&gt;# Access full JWT payload validated by middleware&lt;/span&gt;
    &lt;span class="n"&gt;payload&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;RackJwtAegis&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;RequestContext&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;env&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;user_roles&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'role_ids'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

    &lt;span class="c1"&gt;# Your Rails API business logic here&lt;/span&gt;
    &lt;span class="n"&gt;reports&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Report&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;where&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;company_group_id: &lt;/span&gt;&lt;span class="n"&gt;tenant_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;render&lt;/span&gt; &lt;span class="ss"&gt;json: &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="ss"&gt;message: &lt;/span&gt;&lt;span class="s2"&gt;"Reports retrieved successfully"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;count: &lt;/span&gt;&lt;span class="n"&gt;reports&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;count&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;h3&gt;
  
  
  Multi-Tenant Context Access
&lt;/h3&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# app/controllers/application_controller.rb&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ApplicationController&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;ActionController&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;API&lt;/span&gt;
  &lt;span class="kp"&gt;protected&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;current_user_id&lt;/span&gt;
    &lt;span class="no"&gt;RackJwtAegis&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;RequestContext&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;current_user_id&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;current_tenant_id&lt;/span&gt;
    &lt;span class="no"&gt;RackJwtAegis&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;RequestContext&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;current_tenant_id&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;accessible_companies&lt;/span&gt;
    &lt;span class="no"&gt;RackJwtAegis&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;RequestContext&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;pathname_slugs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;env&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;has_company_access?&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;company_slug&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="no"&gt;RackJwtAegis&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;RequestContext&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;has_pathname_slug_access?&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;env&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;company_slug&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;jwt_payload&lt;/span&gt;
    &lt;span class="no"&gt;RackJwtAegis&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;RequestContext&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;env&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;h3&gt;
  
  
  Available RequestContext Methods
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;authenticated?(env)&lt;/code&gt; - Check if middleware validated the request&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;payload(env)&lt;/code&gt; - Get full JWT payload hash validated by middleware&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;user_id(env)&lt;/code&gt; - Get authenticated user ID&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;tenant_id(env)&lt;/code&gt; - Get tenant/company group ID&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;subdomain(env)&lt;/code&gt; - Get subdomain from JWT&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;pathname_slugs(env)&lt;/code&gt; - Get array of accessible company slugs&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;current_user_id(request)&lt;/code&gt; - Helper for request objects&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;current_tenant_id(request)&lt;/code&gt; - Helper for request objects&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;has_pathname_slug_access?(env, slug)&lt;/code&gt; - Check company access&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Key Benefits&lt;/strong&gt;: Your Rails API controllers receive pre-validated data from the middleware layer, eliminating the need for JWT parsing or multi-tenant validation in your application code.&lt;/p&gt;
&lt;h3&gt;
  
  
  🚀 Building API Gateway-Like Protection
&lt;/h3&gt;

&lt;p&gt;For production Rails API-only applications, combine RackJwtAegis with Rack::Attack for comprehensive API gateway-like middleware protection:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# config/application.rb - Production API security stack&lt;/span&gt;
&lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;middleware&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;insert_before&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="no"&gt;RackJwtAegis&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Middleware&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="ss"&gt;jwt_secret: &lt;/span&gt;&lt;span class="no"&gt;ENV&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'JWT_SECRET'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="ss"&gt;tenant_id_header_name: &lt;/span&gt;&lt;span class="s1"&gt;'X-Company-Group-Id'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="ss"&gt;validate_tenant_id: &lt;/span&gt;&lt;span class="kp"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="ss"&gt;validate_pathname_slug: &lt;/span&gt;&lt;span class="kp"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="ss"&gt;validate_subdomain: &lt;/span&gt;&lt;span class="kp"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="ss"&gt;skip_paths: &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'/api/v1/auth/login'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'/up'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'/api/v1/health'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;# Add rate limiting and request filtering after JWT validation&lt;/span&gt;
&lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;middleware&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;insert_after&lt;/span&gt; &lt;span class="no"&gt;RackJwtAegis&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Middleware&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="no"&gt;Rack&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Attack&lt;/span&gt;
&lt;span class="c1"&gt;# or config.middleware.insert_before RackJwtAegis::Middleware, Rack::Attack&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Result&lt;/strong&gt;: Your Rails API-only application now has enterprise-grade middleware protection:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Layer 1&lt;/strong&gt;: JWT authentication and multi-tenant authorization (RackJwtAegis)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Layer 2&lt;/strong&gt;: Rate limiting, IP blocking, and request filtering (Rack::Attack)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Layer 3&lt;/strong&gt;: Your Rails API controllers with clean, validated data&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This middleware stack provides API gateway-like security without the complexity of external infrastructure, perfect for Rails API applications serving mobile apps, SPAs, and microservices.&lt;/p&gt;




&lt;h2&gt;
  
  
  🔐 JWT Payload Design for Multi-Tenancy
&lt;/h2&gt;

&lt;p&gt;The key to effective multi-tenant authentication is a well-structured JWT payload:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# app/models/user.rb&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;jwt_payload_data&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="ss"&gt;sub: &lt;/span&gt;&lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="c1"&gt;# Standard JWT subject claim&lt;/span&gt;
    &lt;span class="ss"&gt;email: &lt;/span&gt;&lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="ss"&gt;first_name: &lt;/span&gt;&lt;span class="n"&gt;first_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="ss"&gt;last_name: &lt;/span&gt;&lt;span class="n"&gt;last_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="ss"&gt;company_group_id: &lt;/span&gt;&lt;span class="n"&gt;company_group&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;           &lt;span class="c1"&gt;# Tenant isolation&lt;/span&gt;
    &lt;span class="ss"&gt;company_group_domain: &lt;/span&gt;&lt;span class="n"&gt;company_group&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;domain_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;# Subdomain validation&lt;/span&gt;
    &lt;span class="ss"&gt;company_slugs: &lt;/span&gt;&lt;span class="n"&gt;companies&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;pluck&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:slug&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;        &lt;span class="c1"&gt;# Path-based authorization&lt;/span&gt;
    &lt;span class="ss"&gt;iat: &lt;/span&gt;&lt;span class="no"&gt;Time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;current&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;to_i&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;                       &lt;span class="c1"&gt;# Issued at&lt;/span&gt;
    &lt;span class="ss"&gt;exp: &lt;/span&gt;&lt;span class="mi"&gt;24&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;hours&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;from_now&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;to_i&lt;/span&gt;                   &lt;span class="c1"&gt;# Expires at&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This payload structure enables three layers of validation:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Tenant ID validation&lt;/strong&gt; via &lt;code&gt;company_group_id&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Subdomain validation&lt;/strong&gt; via &lt;code&gt;company_group_domain&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Path-based authorization&lt;/strong&gt; via &lt;code&gt;company_slugs&lt;/code&gt;
&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  🏢 Middleware-Level Multi-Tenant Authorization
&lt;/h2&gt;

&lt;p&gt;RackJwtAegis operates at the &lt;strong&gt;Rack middleware layer&lt;/strong&gt;, validating requests before they reach your application. This provides three layers of security validation:&lt;/p&gt;

&lt;h3&gt;
  
  
  URL Structure &amp;amp; Validation
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Request: GET https://acme-corp.localhost.local:3000/api/v1/acme-manufacturing/accounting/reports
Headers: Authorization: Bearer &amp;lt;JWT_TOKEN&amp;gt;
         X-Company-Group-Id: 1

URL Components:
├── Subdomain: acme-corp (maps to company_group_domain in JWT)
├── Path Slug: acme-manufacturing (must be in company_slugs array)
└── Module: accounting (validated in Part 2: RBAC)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Middleware Security Validation
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Before your Rails application receives the request&lt;/strong&gt;, RackJwtAegis middleware validates:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;JWT Token Validity&lt;/strong&gt; ✓ - Signature and expiration check&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tenant Header Match&lt;/strong&gt; ✓ - &lt;code&gt;X-Company-Group-Id&lt;/code&gt; must match JWT &lt;code&gt;company_group_id&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Subdomain Validation&lt;/strong&gt; ✓ - &lt;code&gt;acme-corp.localhost.local&lt;/code&gt; must match JWT &lt;code&gt;company_group_domain&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Path Authorization&lt;/strong&gt; ✓ - &lt;code&gt;acme-manufacturing&lt;/code&gt; must exist in JWT &lt;code&gt;company_slugs&lt;/code&gt; array&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Only valid requests reach your Rails controllers&lt;/strong&gt; - invalid requests are rejected at the middleware layer with appropriate HTTP status codes.&lt;/p&gt;

&lt;h3&gt;
  
  
  Development Setup
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Update /etc/hosts for subdomain testing&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"127.0.0.1 acme-corp.localhost.local"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; /etc/hosts

&lt;span class="c"&gt;# In config/environments/development.rb&lt;/span&gt;
config.hosts &amp;lt;&amp;lt; &lt;span class="s1"&gt;'.localhost.local'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  🔄 Complete Authentication Flow
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. User Login
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST http://localhost:3000/api/v1/auth/login &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{
    "auth": {
      "email": "owner@acme-corp.com",
      "password": "password123"
    }
  }'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Response:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"message"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Login successful"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"token"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9..."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"user"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"email"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"owner@acme-corp.com"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"role"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"super_admin"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"accessible_companies"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="s2"&gt;"acme-manufacturing"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="s2"&gt;"acme-retail"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="s2"&gt;"acme-logistics"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="s2"&gt;"acme-technology"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="s2"&gt;"acme-wholesale"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"expires_at"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2025-08-15T00:36:15Z"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  2. Multi-Tenant API Access
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# ✅ SUCCESS: Valid token + tenant header + authorized company&lt;/span&gt;
curl &lt;span class="nt"&gt;-X&lt;/span&gt; GET http://localhost:3000/api/v1/acme-manufacturing/accounting/reports &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: Bearer &lt;/span&gt;&lt;span class="nv"&gt;$TOKEN&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"X-Company-Group-Id: 1"&lt;/span&gt;

&lt;span class="c"&gt;# ❌ FORBIDDEN: Wrong tenant header&lt;/span&gt;
curl &lt;span class="nt"&gt;-X&lt;/span&gt; GET http://localhost:3000/api/v1/acme-manufacturing/accounting/reports &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: Bearer &lt;/span&gt;&lt;span class="nv"&gt;$TOKEN&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"X-Company-Group-Id: 999"&lt;/span&gt;

&lt;span class="c"&gt;# ❌ FORBIDDEN: Unauthorized company slug&lt;/span&gt;
curl &lt;span class="nt"&gt;-X&lt;/span&gt; GET http://localhost:3000/api/v1/unauthorized-company/accounting/reports &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: Bearer &lt;/span&gt;&lt;span class="nv"&gt;$TOKEN&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"X-Company-Group-Id: 1"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  🧪 Testing Middleware Security
&lt;/h2&gt;

&lt;p&gt;The demo includes curl tests that demonstrate how the middleware rejects requests before they reach your Rails application:&lt;/p&gt;

&lt;h3&gt;
  
  
  JWT Token Validation
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# ❌ No token provided&lt;/span&gt;
curl &lt;span class="nt"&gt;-X&lt;/span&gt; GET http://localhost:3000/api/v1/acme-retail/accounting/reports

&lt;span class="c"&gt;# ❌ Invalid token&lt;/span&gt;
curl &lt;span class="nt"&gt;-X&lt;/span&gt; GET http://localhost:3000/api/v1/acme-retail/accounting/reports &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: Bearer invalid.jwt.token"&lt;/span&gt;

&lt;span class="c"&gt;# ❌ Malformed token&lt;/span&gt;
curl &lt;span class="nt"&gt;-X&lt;/span&gt; GET http://localhost:3000/api/v1/acme-retail/accounting/reports &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: Bearer notjwttoken"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Tenant Isolation Tests
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# ❌ Wrong tenant header&lt;/span&gt;
curl &lt;span class="nt"&gt;-X&lt;/span&gt; GET http://localhost:3000/api/v1/acme-retail/accounting/reports &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: Bearer &lt;/span&gt;&lt;span class="nv"&gt;$VALID_TOKEN&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"X-Company-Group-Id: 999"&lt;/span&gt;

&lt;span class="c"&gt;# ❌ Missing tenant header&lt;/span&gt;
curl &lt;span class="nt"&gt;-X&lt;/span&gt; GET http://localhost:3000/api/v1/acme-retail/accounting/reports &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: Bearer &lt;/span&gt;&lt;span class="nv"&gt;$VALID_TOKEN&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Path-Based Authorization Tests
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# ❌ Unauthorized company slug (not in JWT company_slugs array)&lt;/span&gt;
curl &lt;span class="nt"&gt;-X&lt;/span&gt; GET http://localhost:3000/api/v1/unauthorized-company/accounting/reports &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: Bearer &lt;/span&gt;&lt;span class="nv"&gt;$VALID_TOKEN&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"X-Company-Group-Id: 1"&lt;/span&gt;

&lt;span class="c"&gt;# ✅ Authorized company slug (in JWT company_slugs array)&lt;/span&gt;
curl &lt;span class="nt"&gt;-X&lt;/span&gt; GET http://localhost:3000/api/v1/acme-manufacturing/accounting/reports &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: Bearer &lt;/span&gt;&lt;span class="nv"&gt;$VALID_TOKEN&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"X-Company-Group-Id: 1"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  🎯 What's Coming in Part 2
&lt;/h2&gt;

&lt;p&gt;Part 2 of this series will cover advanced RBAC and caching strategies:&lt;/p&gt;

&lt;h3&gt;
  
  
  Advanced Features Preview
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;🔐 Role-Based Access Control (RBAC)&lt;/strong&gt;: Complex permission hierarchies with role inheritance&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;⚡ Solid Cache Integration&lt;/strong&gt;: High-performance permission caching strategies&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;📊 Permission Matrix&lt;/strong&gt;: Role-based access patterns across modules and companies&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;🎛️ Dynamic Permissions&lt;/strong&gt;: Runtime permission validation and caching&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;🔄 Cache Invalidation&lt;/strong&gt;: Smart cache invalidation strategies for role changes&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  RBAC Permissions Hash Structure
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Coming in Part 2: Cached permissions hash&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="s2"&gt;"permissions"&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="s2"&gt;"last_update"&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;1755141074&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s2"&gt;"1"&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"accounting/*:*"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"inventory/*:*"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"sales/*:*"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;  &lt;span class="c1"&gt;# Owner role&lt;/span&gt;
    &lt;span class="s2"&gt;"11"&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"sales/leads:get"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"sales/customers:get"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;       &lt;span class="c1"&gt;# Sales Rep role&lt;/span&gt;
    &lt;span class="s2"&gt;"14"&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"procurement/*:*"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;                               &lt;span class="c1"&gt;# Procurement Manager&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  🎉 Key Features Covered in Part 1
&lt;/h2&gt;

&lt;h3&gt;
  
  
  ✅ Middleware-Level Security Foundation
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;🔐 JWT Authentication&lt;/strong&gt;: Token validation with HS256 algorithm at middleware layer&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;🏢 Multi-Tenancy&lt;/strong&gt;: Header-based tenant isolation (&lt;code&gt;X-Company-Group-Id&lt;/code&gt;) before Rails&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;🌐 Subdomain Validation&lt;/strong&gt;: Request host validation against JWT claims&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;📍 Path Authorization&lt;/strong&gt;: Company slug validation from JWT payload&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;🛡️ Pre-Application Security&lt;/strong&gt;: Three-tier validation before requests reach controllers&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;🔧 Configurable&lt;/strong&gt;: Custom payload mapping and skip paths&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;🧪 Tested&lt;/strong&gt;: Curl test suite demonstrating middleware rejection patterns&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  🚀 Production-Ready Configuration
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Production multi-tenant setup&lt;/span&gt;
&lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;middleware&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;insert_before&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="no"&gt;RackJwtAegis&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Middleware&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="ss"&gt;jwt_secret: &lt;/span&gt;&lt;span class="no"&gt;ENV&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'JWT_SECRET'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;                    &lt;span class="c1"&gt;# Strong secret required&lt;/span&gt;
  &lt;span class="ss"&gt;tenant_id_header_name: &lt;/span&gt;&lt;span class="s1"&gt;'X-Company-Group-Id'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;     &lt;span class="c1"&gt;# Consistent header naming&lt;/span&gt;
  &lt;span class="ss"&gt;validate_tenant_id: &lt;/span&gt;&lt;span class="kp"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;                        &lt;span class="c1"&gt;# Enable tenant isolation&lt;/span&gt;
  &lt;span class="ss"&gt;validate_subdomain: &lt;/span&gt;&lt;span class="kp"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;                        &lt;span class="c1"&gt;# Enable subdomain validation&lt;/span&gt;
  &lt;span class="ss"&gt;validate_pathname_slug: &lt;/span&gt;&lt;span class="kp"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;                    &lt;span class="c1"&gt;# Enable path authorization&lt;/span&gt;
  &lt;span class="ss"&gt;skip_paths: &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'/api/v1/auth/*'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'/health'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;      &lt;span class="c1"&gt;# Public endpoints&lt;/span&gt;
  &lt;span class="ss"&gt;debug: &lt;/span&gt;&lt;span class="kp"&gt;false&lt;/span&gt;                                      &lt;span class="c1"&gt;# Disable debug in production&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  🔍 Real-World Multi-Tenant Use Cases
&lt;/h2&gt;

&lt;p&gt;The middleware-level authentication patterns covered in Part 1 work well for:&lt;/p&gt;

&lt;h3&gt;
  
  
  Enterprise SaaS Applications
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Multi-tenant CRM/ERP systems&lt;/strong&gt; with company isolation&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Team collaboration platforms&lt;/strong&gt; with workspace separation&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Project management tools&lt;/strong&gt; with client segregation&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;E-commerce marketplaces&lt;/strong&gt; with vendor separation&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  API-First Applications
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Microservices authentication&lt;/strong&gt; with tenant-aware routing&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Mobile app backends&lt;/strong&gt; with organization-based access&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Third-party integrations&lt;/strong&gt; with client-specific endpoints&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Multi-brand platforms&lt;/strong&gt; with subdomain-based routing&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  🎯 Conclusion
&lt;/h2&gt;

&lt;p&gt;In Part 1, we've covered middleware-level multi-tenant JWT authentication with RackJwtAegis. You now have:&lt;/p&gt;

&lt;p&gt;✅ &lt;strong&gt;Three-layer middleware security validation&lt;/strong&gt; (JWT → Tenant → Path)&lt;br&gt;
✅ &lt;strong&gt;Pre-application request filtering&lt;/strong&gt; that protects your Rails controllers&lt;br&gt;
✅ &lt;strong&gt;Production-ready multi-tenant configuration&lt;/strong&gt;&lt;br&gt;
✅ &lt;strong&gt;Testing patterns&lt;/strong&gt; that demonstrate middleware behavior&lt;/p&gt;

&lt;p&gt;This middleware foundation provides security for multi-tenant applications before requests reach your application code, but enterprise applications need more sophisticated authorization patterns.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Part 2 will cover&lt;/strong&gt; RBAC systems, caching strategies, and permission hierarchies for multi-organizational applications.&lt;/p&gt;
&lt;h3&gt;
  
  
  Try the Demo Today
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Prerequisites&lt;/strong&gt;: Ruby 3.3+, Rails 8.0+&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Clone and run the proof-of-concept demo&lt;/span&gt;
git clone https://github.com/kanutocd/rack-jwt-aegis-example
&lt;span class="nb"&gt;cd &lt;/span&gt;rack-jwt-aegis-example

&lt;span class="c"&gt;# Install dependencies&lt;/span&gt;
bundle &lt;span class="nb"&gt;install&lt;/span&gt;

&lt;span class="c"&gt;# Setup database with test users and permissions cache&lt;/span&gt;
rails db:setup

&lt;span class="c"&gt;# Start the server&lt;/span&gt;
rails server

&lt;span class="c"&gt;# Test multi-tenant authentication (returns demo success messages)&lt;/span&gt;
curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST http://localhost:3000/api/v1/auth/login &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{"auth": {"email": "owner@acme-corp.com", "password": "password123"}}'&lt;/span&gt;

&lt;span class="c"&gt;# Use the returned JWT token to test middleware security&lt;/span&gt;
&lt;span class="nv"&gt;TOKEN&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&amp;lt;paste-token-here&amp;gt;"&lt;/span&gt;
curl &lt;span class="nt"&gt;-X&lt;/span&gt; GET http://localhost:3000/api/v1/acme-manufacturing/accounting/reports &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: Bearer &lt;/span&gt;&lt;span class="nv"&gt;$TOKEN&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"X-Company-Group-Id: 1"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;What you'll see&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Authentication endpoint returns a JWT token with multi-tenant claims&lt;/li&gt;
&lt;li&gt;Protected endpoints return success messages like &lt;code&gt;{"message": "Reports retrieved successfully"}&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Invalid requests are rejected by middleware before reaching Rails controllers&lt;/li&gt;
&lt;li&gt;Comprehensive test users with different permission levels (see &lt;code&gt;CURL_TESTING_GUIDE.md&lt;/code&gt;)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Remember&lt;/strong&gt;: This demo only returns success messages to show middleware authentication patterns - no actual ERP functionality is implemented.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;👀 Watch for Part 2&lt;/strong&gt;: Advanced RBAC and Caching Strategies with RackJwtAegis&lt;/p&gt;




&lt;h2&gt;
  
  
  📚 Resources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;📦 RubyGems&lt;/strong&gt;: &lt;a href="https://rubygems.org/gems/rack_jwt_aegis" rel="noopener noreferrer"&gt;rack_jwt_aegis&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;📖 GitHub Repository&lt;/strong&gt;: &lt;a href="https://github.com/kanutocd/rack_jwt_aegis" rel="noopener noreferrer"&gt;RackJwtAegis&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;💻 Demo App&lt;/strong&gt;: &lt;a href="https://github.com/kanutocd/rack-jwt-aegis-example" rel="noopener noreferrer"&gt;Multi-Tenant Demo (Proof-of-Concept)&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;🧪 Testing Guide&lt;/strong&gt;: &lt;a href="https://github.com/kanutocd/rack-jwt-aegis-example/blob/main/CURL_TESTING_GUIDE.md" rel="noopener noreferrer"&gt;Complete curl Testing Suite&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;Found this helpful? Give it a ⭐ on GitHub and watch for Part 2!&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Tags&lt;/strong&gt;: #ruby #rails #jwt #authentication #middleware #multitenant #security #api #saas #enterprise&lt;/p&gt;

</description>
      <category>rails</category>
      <category>jwt</category>
      <category>middleware</category>
      <category>multitenant</category>
    </item>
  </channel>
</rss>
