jetstream_bridge 3.0.2 → 4.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/README.md CHANGED
@@ -31,43 +31,64 @@
31
31
  </p>
32
32
 
33
33
  <p align="center">
34
+ <a href="#-why-jetstream-bridge">Why?</a> •
34
35
  <a href="#-features">Features</a> •
35
- <a href="#-install">Install</a> •
36
- <a href="#-getting-started">Getting Started</a> •
37
- <a href="#-operations-guide">Operations</a>
36
+ <a href="#-quick-start">Quick Start</a> •
37
+ <a href="#-documentation">Documentation</a> •
38
+ <a href="#-contributing">Contributing</a>
38
39
  </p>
39
40
 
40
41
  ---
41
42
 
43
+ ## 🎯 Why JetStream Bridge?
44
+
45
+ Building event-driven systems with NATS JetStream is powerful, but comes with challenges:
46
+
47
+ * **Reliability**: How do you guarantee messages aren't lost during deploys or network failures?
48
+ * **Idempotency**: How do you prevent duplicate processing when messages are redelivered?
49
+ * **Stream Management**: How do you avoid "subjects overlap" errors in production?
50
+ * **Monitoring**: How do you know if your consumers are healthy and processing messages?
51
+ * **Rails Integration**: How do you integrate cleanly with ActiveRecord transactions?
52
+
53
+ **JetStream Bridge solves these problems** with production-tested patterns:
54
+
55
+ * ✅ **Transactional Outbox** - Never lose events, even if NATS is down
56
+ * ✅ **Idempotent Inbox** - Process each message exactly once, safely
57
+ * ✅ **Automatic Stream Provisioning** - No more manual stream management or overlap conflicts
58
+ * ✅ **Built-in Health Checks** - K8s-ready monitoring endpoints
59
+ * ✅ **Rails-Native** - Works seamlessly with ActiveRecord and Rails conventions
60
+
61
+ ---
62
+
42
63
  ## ✨ Features
43
64
 
44
65
  ### Core Capabilities
45
66
 
46
- * 🔌 Simple **Publisher** and **Consumer** interfaces
47
- * 🛡 **Outbox** (reliable send) & **Inbox** (idempotent receive), opt-in
48
- * 🧨 **DLQ** for poison messages
49
- * ⚙️ Durable `pull_subscribe` with backoff & `max_deliver`
50
- * 🎯 Clear **source/destination** subject conventions
51
- * 🧱 **Overlap-safe stream ensure** (prevents "subjects overlap" BadRequest)
52
- * 🚂 **Rails generators** for initializer & migrations, plus an install **rake task**
53
- * ⚡️ **Eager-loaded models** via Railtie (production)
54
- * 📊 Configurable logging with sensible defaults
55
-
56
- ### Production-Ready Features
57
-
58
- * 🏥 **Health checks** - Monitor NATS connection and stream status
59
- * 🔄 **Auto-reconnection** - Automatic recovery from connection failures
60
- * 🔒 **Race condition protection** - Pessimistic locking for outbox operations
61
- * 🛡️ **Transaction safety** - All database operations wrapped in transactions
62
- * 🎯 **Subject validation** - Prevents NATS wildcards in configuration
63
- * 🚦 **Graceful shutdown** - Signal handlers and message draining
64
- * 📈 **Retry strategies** - Pluggable exponential/linear backoff algorithms
65
- * 🎨 **Value objects** - Type-safe domain models for events and subjects
66
- * 🏗️ **SOLID architecture** - Clean separation of concerns and dependency injection
67
+ * 🔌 **Simple Publisher and Consumer interfaces** - Write event-driven code in minutes with intuitive APIs that abstract NATS complexity
68
+ * 🛡 **Outbox & Inbox patterns (opt-in)** - Guarantee exactly-once delivery with reliable send (Outbox) and idempotent receive (Inbox)
69
+ * 🧨 **Dead Letter Queue (DLQ)** - Isolate and triage poison messages automatically instead of blocking your entire pipeline
70
+ * ⚙️ **Durable pull subscriptions** - Never lose messages with configurable backoff strategies and max delivery attempts
71
+ * 🎯 **Clear subject conventions** - Organized source/destination routing that scales across multiple services
72
+ * 🧱 **Overlap-safe stream provisioning** - Automatically prevents "subjects overlap" errors with intelligent conflict detection
73
+ * 🚂 **Rails generators included** - Generate initializers, migrations, and health checks with a single command
74
+ * ⚡️ **Zero-downtime deploys** - Eager-loaded models via Railtie ensure production stability
75
+ * 📊 **Observable by default** - Configurable logging with sensible defaults for debugging and monitoring
76
+
77
+ ### Production-Ready Reliability
78
+
79
+ * 🏥 **Built-in health checks** - Monitor NATS connection, stream status, and configuration for K8s readiness/liveness probes
80
+ * 🔄 **Automatic reconnection** - Recover from network failures and NATS restarts without manual intervention
81
+ * 🔒 **Race condition protection** - Pessimistic locking prevents duplicate publishes in high-concurrency scenarios
82
+ * 🛡️ **Transaction safety** - All database operations are atomic with automatic rollback on failures
83
+ * 🎯 **Subject validation** - Catch configuration errors early by preventing NATS wildcards where they don't belong
84
+ * 🚦 **Graceful shutdown** - Proper signal handling and message draining prevent data loss during deploys
85
+ * 📈 **Pluggable retry strategies** - Choose exponential or linear backoff, or implement your own custom strategy
67
86
 
68
87
  ---
69
88
 
70
- ## 📦 Install
89
+ ## 🚀 Quick Start
90
+
91
+ ### 1. Install the Gem
71
92
 
72
93
  ```ruby
73
94
  # Gemfile
@@ -78,6 +99,76 @@ gem "jetstream_bridge", "~> 3.0"
78
99
  bundle install
79
100
  ```
80
101
 
102
+ ### 2. Generate Configuration and Migrations
103
+
104
+ ```bash
105
+ # Creates initializer and migrations
106
+ bin/rails g jetstream_bridge:install
107
+
108
+ # Run migrations
109
+ bin/rails db:migrate
110
+ ```
111
+
112
+ ### 3. Configure Your Application
113
+
114
+ ```ruby
115
+ # config/initializers/jetstream_bridge.rb
116
+ JetstreamBridge.configure do |config|
117
+ config.nats_urls = ENV.fetch("NATS_URLS", "nats://localhost:4222")
118
+ config.env = ENV.fetch("RAILS_ENV", "development")
119
+ config.app_name = "my_app"
120
+ config.destination_app = "other_app" # Required: The app you're communicating with
121
+
122
+ # Enable reliability features (recommended for production)
123
+ config.use_outbox = true # Transactional outbox pattern
124
+ config.use_inbox = true # Idempotent message processing
125
+ config.use_dlq = true # Dead letter queue for failed messages
126
+ end
127
+ ```
128
+
129
+ ### 4. Publish Your First Event
130
+
131
+ ```ruby
132
+ # In your Rails application
133
+ JetstreamBridge.publish(
134
+ resource_type: "user",
135
+ event_type: "created",
136
+ payload: { id: user.id, email: user.email }
137
+ )
138
+ ```
139
+
140
+ ### 5. Consume Events
141
+
142
+ ```ruby
143
+ # Create a consumer (e.g., in a rake task or separate process)
144
+ consumer = JetstreamBridge::Consumer.new do |event|
145
+ user_data = event.payload
146
+ # Your idempotent business logic here
147
+ User.find_or_create_by(id: user_data.id) do |user|
148
+ user.email = user_data.email
149
+ end
150
+ end
151
+
152
+ consumer.run! # Starts consuming messages
153
+ ```
154
+
155
+ That's it! You're now publishing and consuming events with JetStream.
156
+
157
+ ---
158
+
159
+ ## 📖 Documentation
160
+
161
+ ### Table of Contents
162
+
163
+ * [Installation & Setup](#-rails-generators--rake-tasks)
164
+ * [Configuration](#-configuration)
165
+ * [Publishing Events](#-publish-events)
166
+ * [Consuming Events](#-consume-events)
167
+ * [Database Setup](#-database-setup-inbox--outbox)
168
+ * [Stream Topology](#-stream-topology-auto-ensure-and-overlap-safe)
169
+ * [Operations Guide](#-operations-guide)
170
+ * [Troubleshooting](#-troubleshooting)
171
+
81
172
  ---
82
173
 
83
174
  ## 🧰 Rails Generators & Rake Tasks
@@ -129,59 +220,178 @@ bin/rake jetstream_bridge:debug
129
220
 
130
221
  ---
131
222
 
132
- ## 🔧 Configure (Rails)
223
+ ## 🔧 Configuration
224
+
225
+ ### Basic Configuration
133
226
 
134
227
  ```ruby
135
228
  # config/initializers/jetstream_bridge.rb
136
229
  JetstreamBridge.configure do |config|
137
- # NATS connection
138
- config.nats_urls = ENV.fetch("NATS_URLS", "nats://localhost:4222")
139
- config.env = ENV.fetch("NATS_ENV", "development")
140
- config.app_name = ENV.fetch("APP_NAME", "app")
141
- config.destination_app = ENV["DESTINATION_APP"] # required
230
+ # === Required Settings ===
142
231
 
143
- # Consumer tuning
144
- config.max_deliver = 5
145
- config.ack_wait = "30s"
146
- config.backoff = %w[1s 5s 15s 30s 60s]
232
+ # NATS server URLs (comma-separated for multiple servers)
233
+ config.nats_urls = ENV.fetch("NATS_URLS", "nats://localhost:4222")
147
234
 
148
- # Reliability features (opt-in)
235
+ # Environment namespace (development, staging, production)
236
+ config.env = ENV.fetch("RAILS_ENV", "development")
237
+
238
+ # Your application name (used in subject routing)
239
+ config.app_name = ENV.fetch("APP_NAME", "my_app")
240
+
241
+ # The application you're communicating with (REQUIRED)
242
+ config.destination_app = ENV.fetch("DESTINATION_APP")
243
+
244
+ # === Reliability Features (Recommended for Production) ===
245
+
246
+ # Transactional Outbox: Ensures events are never lost
149
247
  config.use_outbox = true
150
- config.use_inbox = true
151
- config.use_dlq = true
152
248
 
153
- # Models (override if you use custom AR classes/table names)
154
- config.outbox_model = "JetstreamBridge::OutboxEvent"
155
- config.inbox_model = "JetstreamBridge::InboxEvent"
249
+ # Idempotent Inbox: Prevents duplicate processing
250
+ config.use_inbox = true
251
+
252
+ # Dead Letter Queue: Isolates poison messages
253
+ config.use_dlq = true
254
+
255
+ # === Consumer Tuning ===
256
+
257
+ # Maximum delivery attempts before moving to DLQ
258
+ config.max_deliver = 5
259
+
260
+ # Time to wait for acknowledgment before redelivery
261
+ config.ack_wait = "30s"
262
+
263
+ # Backoff delays between retries (exponential backoff)
264
+ config.backoff = %w[1s 5s 15s 30s 60s]
265
+
266
+ # === Advanced Options ===
156
267
 
157
- # Logging
268
+ # Custom ActiveRecord models (if you have your own tables)
269
+ # config.outbox_model = "CustomOutboxEvent"
270
+ # config.inbox_model = "CustomInboxEvent"
271
+
272
+ # Custom logger
158
273
  # config.logger = Rails.logger
159
274
  end
160
275
  ```
161
276
 
162
- > **Defaults:**
163
- >
164
- > * `stream_name` `#{env}-jetstream-bridge-stream`
165
- > * `dlq_subject` → `#{env}.sync.dlq`
277
+ ### Configuration Reference
278
+
279
+ | Setting | Type | Default | Description |
280
+ |---------|------|---------|-------------|
281
+ | `nats_urls` | String | `"nats://localhost:4222"` | NATS server URL(s), comma-separated |
282
+ | `env` | String | `"development"` | Environment namespace for streams |
283
+ | `app_name` | String | (required) | Your application identifier |
284
+ | `destination_app` | String | (required) | Target application for events |
285
+ | `use_outbox` | Boolean | `false` | Enable transactional outbox pattern |
286
+ | `use_inbox` | Boolean | `false` | Enable idempotent inbox pattern |
287
+ | `use_dlq` | Boolean | `false` | Enable dead letter queue |
288
+ | `max_deliver` | Integer | `5` | Max delivery attempts before DLQ |
289
+ | `ack_wait` | String/Integer | `"30s"` | Acknowledgment timeout |
290
+ | `backoff` | Array | `["1s", "5s", "15s"]` | Retry backoff schedule |
291
+
292
+ ### Understanding Configuration Options
293
+
294
+ #### When to Use Outbox
295
+
296
+ Enable `use_outbox` when you need:
297
+
298
+ * **Transactional guarantees**: Publish events as part of database transactions
299
+ * **Reliability**: Ensure events are never lost, even if NATS is temporarily down
300
+ * **Audit trail**: Keep a permanent record of all published events
301
+
302
+ ```ruby
303
+ # Example: Publishing with outbox ensures atomicity
304
+ ActiveRecord::Base.transaction do
305
+ user.save!
306
+ JetstreamBridge.publish(
307
+ resource_type: "user",
308
+ event_type: "created",
309
+ payload: { id: user.id }
310
+ ) # Event is saved in outbox table first
311
+ end
312
+ ```
313
+
314
+ #### When to Use Inbox
315
+
316
+ Enable `use_inbox` when you need:
317
+
318
+ * **Idempotency**: Process each message exactly once, even with redeliveries
319
+ * **Duplicate protection**: Prevent duplicate processing across restarts
320
+ * **Processing history**: Track which messages have been processed
321
+
322
+ ```ruby
323
+ # Example: Inbox prevents duplicate processing
324
+ # Even if the message is redelivered, it will be skipped
325
+ # if already marked as processed
326
+ ```
327
+
328
+ #### When to Use DLQ
329
+
330
+ Enable `use_dlq` when you need:
331
+
332
+ * **Poison message handling**: Isolate messages that repeatedly fail
333
+ * **Manual intervention**: Review and retry failed messages later
334
+ * **Pipeline protection**: Prevent one bad message from blocking the queue
335
+
336
+ ### Logging Configuration
337
+
338
+ JetstreamBridge integrates with your application's logger:
339
+
340
+ ```ruby
341
+ # Use Rails logger (default)
342
+ config.logger = Rails.logger
343
+
344
+ # Use custom logger
345
+ config.logger = Logger.new(STDOUT)
346
+
347
+ # Disable logging
348
+ config.logger = Logger.new(IO::NULL)
349
+ ```
350
+
351
+ ### Environment Variables
352
+
353
+ Recommended environment variable setup:
354
+
355
+ ```bash
356
+ # .env (or your deployment configuration)
357
+ NATS_URLS=nats://nats1:4222,nats://nats2:4222,nats://nats3:4222
358
+ RAILS_ENV=production
359
+ APP_NAME=api_service
360
+ DESTINATION_APP=notification_service
361
+ ```
362
+
363
+ ### Generated Streams and Subjects
166
364
 
167
- ### Logging
365
+ Based on your configuration, JetStream Bridge automatically creates:
168
366
 
169
- JetstreamBridge logs through `config.logger` when set, falling back to `Rails.logger` or STDOUT. Provide any `Logger`-compatible instance in the initializer to integrate with your application's logging setup.
367
+ * **Stream Name**: `{env}-jetstream-bridge-stream`
368
+ * Example: `production-jetstream-bridge-stream`
369
+
370
+ * **Publish Subject**: `{env}.{app_name}.sync.{destination_app}`
371
+ * Example: `production.api_service.sync.notification_service`
372
+
373
+ * **Subscribe Subject**: `{env}.{destination_app}.sync.{app_name}`
374
+ * Example: `production.notification_service.sync.api_service`
375
+
376
+ * **DLQ Subject**: `{env}.{app}.sync.dlq` (per-app DLQ for isolation)
377
+ * Example: `production.api_service.sync.dlq`
170
378
 
171
379
  ---
172
380
 
173
381
  ## 📡 Subject Conventions
174
382
 
175
- | Direction | Subject Pattern |
176
- |---------------|---------------------------|
177
- | **Publish** | `{env}.{app}.sync.{dest}` |
178
- | **Subscribe** | `{env}.{dest}.sync.{app}` |
179
- | **DLQ** | `{env}.sync.dlq` |
383
+ | Direction | Subject Pattern |
384
+ |---------------|------------------------------|
385
+ | **Publish** | `{env}.{app}.sync.{dest}` |
386
+ | **Subscribe** | `{env}.{dest}.sync.{app}` |
387
+ | **DLQ** | `{env}.{app}.sync.dlq` |
180
388
 
181
389
  * `{app}`: `app_name`
182
390
  * `{dest}`: `destination_app`
183
391
  * `{env}`: `env`
184
392
 
393
+ **Note**: Each application has its own DLQ (`{env}.{app}.sync.dlq`) for better isolation, monitoring, and debugging. This allows you to track failed messages per service.
394
+
185
395
  ---
186
396
 
187
397
  ## 🧱 Stream Topology (auto-ensure and overlap-safe)
@@ -249,51 +459,649 @@ add_index :jetstream_inbox_events, :status
249
459
 
250
460
  ## 📤 Publish Events
251
461
 
462
+ JetStream Bridge provides two ways to publish events: a convenience method and a direct publisher instance.
463
+
464
+ ### Using the Convenience Method to Consume Events
465
+
466
+ The simplest way to publish events:
467
+
468
+ ```ruby
469
+ # Basic usage with structured parameters
470
+ JetstreamBridge.publish(
471
+ resource_type: "user",
472
+ event_type: "created",
473
+ payload: { id: user.id, email: user.email, name: user.name }
474
+ )
475
+
476
+ # Returns true on success, raises error on failure
477
+ ```
478
+
479
+ ### Publishing Patterns
480
+
481
+ #### 1. Structured Parameters (Recommended)
482
+
483
+ Best for explicit, clear code:
484
+
485
+ ```ruby
486
+ JetstreamBridge.publish(
487
+ resource_type: "user",
488
+ event_type: "created",
489
+ payload: { id: "01H...", email: "ada@example.com" },
490
+ # Optional parameters:
491
+ event_id: SecureRandom.uuid, # Auto-generated if not provided
492
+ trace_id: request_id, # For distributed tracing
493
+ occurred_at: Time.now.utc, # Defaults to current time
494
+ subject: "custom.subject.override" # Override default subject
495
+ )
496
+ ```
497
+
498
+ #### 2. Simplified Hash (Infers resource_type)
499
+
500
+ Use dot notation in `event_type` to infer `resource_type`:
501
+
502
+ ```ruby
503
+ JetstreamBridge.publish(
504
+ event_type: "user.created", # "user" becomes resource_type
505
+ payload: { id: "01H...", email: "ada@example.com" }
506
+ )
507
+ ```
508
+
509
+ #### 3. Complete Envelope (Advanced)
510
+
511
+ Pass a full envelope hash for maximum control:
512
+
513
+ ```ruby
514
+ JetstreamBridge.publish(
515
+ event_type: "created",
516
+ resource_type: "user",
517
+ payload: { id: "01H...", email: "ada@example.com" },
518
+ event_id: "custom-event-id",
519
+ occurred_at: 1.hour.ago.iso8601,
520
+ producer: "custom-producer"
521
+ )
522
+ ```
523
+
524
+ ### Using Publisher Instances
525
+
526
+ For more control or batch operations:
527
+
252
528
  ```ruby
253
529
  publisher = JetstreamBridge::Publisher.new
530
+
531
+ # Publish multiple events
254
532
  publisher.publish(
255
533
  resource_type: "user",
256
- event_type: "created",
257
- payload: { id: "01H...", name: "Ada" }, # resource_id inferred from payload[:id] / payload["id"]
258
- # optional:
259
- # event_id: "uuid-or-ulid",
260
- # trace_id: "hex",
261
- # occurred_at: Time.now.utc
534
+ event_type: "created",
535
+ payload: { id: user.id }
536
+ )
537
+
538
+ publisher.publish(
539
+ resource_type: "user",
540
+ event_type: "updated",
541
+ payload: { id: user.id, email: user.email }
262
542
  )
263
543
  ```
264
544
 
265
- If **Outbox** is enabled, the publish call:
545
+ #### Thread Safety
546
+
547
+ Publisher instances are **thread-safe** and can be shared across multiple threads:
266
548
 
267
- * Upserts an outbox row by `event_id`
268
- * Publishes with `nats-msg-id` (idempotent)
269
- * Marks status `sent` or records `failed` with `last_error`
549
+ ```ruby
550
+ # Safe: Share publisher across threads
551
+ @publisher = JetstreamBridge::Publisher.new
552
+
553
+ # Multiple threads can publish concurrently
554
+ threads = 10.times.map do |i|
555
+ Thread.new do
556
+ @publisher.publish(
557
+ event_type: "user.created",
558
+ payload: { id: i, name: "User #{i}" }
559
+ )
560
+ end
561
+ end
562
+
563
+ threads.each(&:join)
564
+ ```
565
+
566
+ The underlying NATS connection is managed through a thread-safe singleton, ensuring safe concurrent access. However, for high-throughput scenarios, consider:
567
+
568
+ * Using the convenience method `JetstreamBridge.publish(...)` which handles connection management automatically
569
+ * Creating a connection pool if you need isolated error handling per thread
570
+ * Using batch publishing for bulk operations: `JetstreamBridge.publish_batch { ... }`
571
+
572
+ ### Publishing with Transactions (Outbox Pattern)
573
+
574
+ When `use_outbox` is enabled, events are saved to the database first:
575
+
576
+ ```ruby
577
+ # Atomic: both user creation and event publishing succeed or fail together
578
+ ActiveRecord::Base.transaction do
579
+ user = User.create!(email: "ada@example.com")
580
+
581
+ JetstreamBridge.publish(
582
+ resource_type: "user",
583
+ event_type: "created",
584
+ payload: { id: user.id, email: user.email }
585
+ )
586
+ end
587
+ # Event is saved in outbox table, published to NATS asynchronously
588
+ ```
589
+
590
+ ### Outbox Behavior
591
+
592
+ If **Outbox** is enabled (`config.use_outbox = true`):
593
+
594
+ * ✅ Events are saved to `jetstream_outbox_events` table first
595
+ * ✅ Published with `nats-msg-id` header for idempotency
596
+ * ✅ Marked as `sent` on success or `failed` with error details
597
+ * ✅ Survives NATS downtime - events queued for retry
598
+ * ✅ Provides audit trail of all published events
599
+
600
+ ### Real-World Publishing Examples
601
+
602
+ #### Publishing Domain Events
603
+
604
+ ```ruby
605
+ # After user registration
606
+ class UsersController < ApplicationController
607
+ def create
608
+ ActiveRecord::Base.transaction do
609
+ @user = User.create!(user_params)
610
+
611
+ JetstreamBridge.publish(
612
+ resource_type: "user",
613
+ event_type: "registered",
614
+ payload: {
615
+ id: @user.id,
616
+ email: @user.email,
617
+ name: @user.name,
618
+ plan: @user.plan
619
+ },
620
+ trace_id: request.request_id
621
+ )
622
+ end
623
+
624
+ redirect_to @user
625
+ end
626
+ end
627
+ ```
628
+
629
+ #### Publishing State Changes
630
+
631
+ ```ruby
632
+ # In your model or service object
633
+ class Order
634
+ after_commit :publish_status_change, if: :saved_change_to_status?
635
+
636
+ private
637
+
638
+ def publish_status_change
639
+ JetstreamBridge.publish(
640
+ resource_type: "order",
641
+ event_type: "status_changed",
642
+ payload: {
643
+ id: id,
644
+ status: status,
645
+ previous_status: status_before_last_save,
646
+ changed_at: updated_at
647
+ }
648
+ )
649
+ end
650
+ end
651
+ ```
652
+
653
+ #### Publishing with Distributed Tracing
654
+
655
+ ```ruby
656
+ # Include trace_id for correlation across services
657
+ JetstreamBridge.publish(
658
+ resource_type: "payment",
659
+ event_type: "processed",
660
+ payload: { order_id: order.id, amount: payment.amount },
661
+ trace_id: Current.request_id,
662
+ event_id: payment.transaction_id
663
+ )
664
+ ```
270
665
 
271
666
  ---
272
667
 
273
668
  ## 📥 Consume Events
274
669
 
670
+ JetStream Bridge provides two ways to consume events: a convenience method and direct consumer instances.
671
+
672
+ ### Using the Convenience Method (Recommended)
673
+
674
+ The simplest way to start consuming events:
675
+
275
676
  ```ruby
276
- JetstreamBridge::Consumer.new do |event, subject, deliveries|
277
- # Your idempotent domain logic here
278
- # `event` is the parsed envelope hash
279
- UserCreatedHandler.call(event["payload"])
280
- end.run!
677
+ # Start consumer and run in current thread (receives Event object)
678
+ consumer = JetstreamBridge.subscribe do |event|
679
+ # event is a Models::Event object with structured access
680
+ puts "Processing: #{event.type}"
681
+ puts "Payload: #{event.payload.to_h}"
682
+ puts "Event ID: #{event.event_id}"
683
+ puts "Deliveries: #{event.deliveries}"
684
+
685
+ User.find_or_create_by(id: event.payload.id) do |user|
686
+ user.email = event.payload.email
687
+ end
688
+ end
689
+
690
+ consumer.run! # Blocks and processes messages
691
+ ```
692
+
693
+ **Note:** The consumer handler receives a structured `Models::Event` object (not a raw hash) with convenient accessor methods for event data and metadata.
694
+
695
+ ### Understanding the Event Object
696
+
697
+ Consumers receive a `JetstreamBridge::Models::Event` object that provides structured access to event data and metadata:
698
+
699
+ ```ruby
700
+ JetstreamBridge.subscribe do |event|
701
+ # Event data
702
+ event.event_id # => "abc-123-def"
703
+ event.type # => "user.created" (event_type)
704
+ event.resource_type # => "user"
705
+ event.resource_id # => "123"
706
+ event.producer # => "api_service"
707
+ event.occurred_at # => 2025-01-15 10:30:00 UTC (Time object)
708
+ event.trace_id # => "xyz789"
709
+
710
+ # Payload access (method-style or hash-style)
711
+ event.payload.id # => 123
712
+ event.payload.email # => "ada@example.com"
713
+ event.payload["id"] # => 123 (also works)
714
+ event.payload.to_h # => { "id" => 123, "email" => "ada@example.com" }
715
+
716
+ # Delivery metadata
717
+ event.deliveries # => 1 (delivery attempt count)
718
+ event.subject # => "production.api.sync.worker"
719
+ event.stream # => "production-jetstream-bridge-stream"
720
+ event.sequence # => 42 (stream sequence number)
721
+
722
+ # Access all metadata
723
+ event.metadata.to_h # => { subject: "...", deliveries: 1, ... }
724
+
725
+ # Convert to hash (for backwards compatibility)
726
+ event.to_h # => Full event as hash
727
+ event["event_type"] # => "user.created" (hash-style access)
728
+ end
729
+ ```
730
+
731
+ This structured approach provides type safety, cleaner code, and better IDE support compared to raw hashes.
732
+
733
+ ### Consuming Patterns
734
+
735
+ #### 1. Basic Consumer with Block
736
+
737
+ ```ruby
738
+ consumer = JetstreamBridge.subscribe do |event|
739
+ # event: Models::Event object with structured access
740
+ # event.type: Event type (e.g., "created", "user.created")
741
+ # event.payload: PayloadAccessor for event data
742
+ # event.deliveries: Number of delivery attempts (starts at 1)
743
+ # event.metadata: Delivery metadata (subject, stream, sequence, etc.)
744
+
745
+ case event.type
746
+ when "created"
747
+ UserCreatedHandler.call(event.payload.to_h)
748
+ when "updated"
749
+ UserUpdatedHandler.call(event.payload.to_h)
750
+ when "deleted"
751
+ UserDeletedHandler.call(event.payload.to_h)
752
+ end
753
+ end
754
+
755
+ consumer.run! # Start consuming
756
+ ```
757
+
758
+ #### 2. Run Consumer in Background Thread
759
+
760
+ ```ruby
761
+ # Returns a Thread instead of Consumer
762
+ thread = JetstreamBridge.subscribe(run: true) do |event|
763
+ ProcessEventJob.perform_later(event.to_h)
764
+ end
765
+
766
+ # Consumer runs in background
767
+ # Your application continues
768
+
769
+ # Later, to stop:
770
+ thread.kill
771
+ ```
772
+
773
+ #### 3. Consumer with Handler Object
774
+
775
+ ```ruby
776
+ class EventHandler
777
+ def call(event)
778
+ logger.info "Processing #{event.type} from #{event.subject} (attempt #{event.deliveries})"
779
+ # Your logic here
780
+ end
781
+ end
782
+
783
+ handler = EventHandler.new
784
+ consumer = JetstreamBridge.subscribe(handler)
785
+ consumer.run!
786
+ ```
787
+
788
+ #### 4. Consumer with Custom Configuration
789
+
790
+ ```ruby
791
+ consumer = JetstreamBridge.subscribe(
792
+ durable_name: "my-custom-consumer",
793
+ batch_size: 10
794
+ ) do |event|
795
+ # Process events in batches of 10
796
+ end
797
+
798
+ consumer.run!
799
+ ```
800
+
801
+ #### 5. Consumer with Middleware
802
+
803
+ Add cross-cutting concerns like logging, metrics, and tracing using middleware:
804
+
805
+ ```ruby
806
+ consumer = JetstreamBridge.subscribe do |event|
807
+ process_event(event)
808
+ end
809
+
810
+ # Add built-in middleware
811
+ consumer.use(JetstreamBridge::Consumer::LoggingMiddleware.new)
812
+ consumer.use(JetstreamBridge::Consumer::MetricsMiddleware.new(
813
+ on_success: ->(event, duration) { StatsD.timing("event.process", duration) },
814
+ on_failure: ->(event, error) { StatsD.increment("event.failed") }
815
+ ))
816
+ consumer.use(JetstreamBridge::Consumer::TimeoutMiddleware.new(timeout: 30))
817
+
818
+ consumer.run!
819
+ ```
820
+
821
+ **Available Middleware:**
822
+
823
+ * `LoggingMiddleware` - Logs event processing start, completion, and errors
824
+ * `ErrorHandlingMiddleware` - Custom error handling with callbacks
825
+ * `MetricsMiddleware` - Track processing metrics and timing
826
+ * `TracingMiddleware` - Distributed tracing support (sets Current.trace_id)
827
+ * `TimeoutMiddleware` - Prevent long-running handlers from blocking
828
+
829
+ **Custom Middleware Example:**
830
+
831
+ ```ruby
832
+ class RetryMiddleware
833
+ def initialize(max_retries: 3)
834
+ @max_retries = max_retries
835
+ end
836
+
837
+ def call(event)
838
+ retries = 0
839
+ begin
840
+ yield
841
+ rescue TransientError => e
842
+ retries += 1
843
+ if retries < @max_retries
844
+ sleep(retries)
845
+ retry
846
+ else
847
+ raise
848
+ end
849
+ end
850
+ end
851
+ end
852
+
853
+ consumer.use(RetryMiddleware.new(max_retries: 5))
854
+ ```
855
+
856
+ ### Using Consumer Instances Directly
857
+
858
+ For more control over the consumer lifecycle:
859
+
860
+ ```ruby
861
+ consumer = JetstreamBridge::Consumer.new do |event|
862
+ # event: Models::Event object with structured access
863
+
864
+ logger.info "Processing #{event.type} (attempt #{event.deliveries})"
865
+
866
+ begin
867
+ process_event(event)
868
+ rescue RecoverableError => e
869
+ # Let JetStream retry with backoff
870
+ raise
871
+ rescue UnrecoverableError => e
872
+ # Log and acknowledge to prevent infinite retries
873
+ logger.error "Unrecoverable error: #{e.message}"
874
+ # Automatically moved to DLQ if configured
875
+ end
876
+ end
877
+
878
+ consumer.run! # Start processing
879
+ ```
880
+
881
+ ### Understanding the Handler Signature
882
+
883
+ Your handler receives a single `Models::Event` parameter with all event and metadata:
884
+
885
+ ```ruby
886
+ JetstreamBridge.subscribe do |event|
887
+ # event: Models::Event - structured event object
888
+
889
+ puts "Event type: #{event.type}"
890
+ puts "Event ID: #{event.event_id}"
891
+ puts "Subject: #{event.subject}"
892
+ puts "Delivery attempt: #{event.deliveries}"
893
+ puts "Trace ID: #{event.trace_id}"
894
+
895
+ # Access payload
896
+ puts "User ID: #{event.payload.id}"
897
+ puts "Email: #{event.payload.email}"
898
+
899
+ # Implement retry logic based on deliveries if needed
900
+ raise "Transient error, retry" if event.deliveries < 3 && some_transient_condition?
901
+ end
902
+ ```
903
+
904
+ ### Consumer Options
905
+
906
+ | Option | Type | Default | Description |
907
+ |--------|------|---------|-------------|
908
+ | `handler` | Proc/Callable | (required) | Block or object that responds to `call` |
909
+ | `run` | Boolean | `false` | Start consumer in background thread |
910
+ | `durable_name` | String | From config | Custom durable consumer name |
911
+ | `batch_size` | Integer | From config | Number of messages to fetch at once |
912
+
913
+ ### Inbox Behavior
914
+
915
+ If **Inbox** is enabled (`config.use_inbox = true`):
916
+
917
+ * ✅ Deduplicates by `event_id` (or stream sequence as fallback)
918
+ * ✅ Records processing state, errors, and timestamps
919
+ * ✅ Skips already-processed messages automatically
920
+ * ✅ Provides processing history and audit trail
921
+ * ✅ Safe across restarts and redeliveries
922
+
923
+ ```ruby
924
+ # With inbox enabled, this is automatically idempotent:
925
+ JetstreamBridge.subscribe do |event|
926
+ # Even if message is redelivered, it won't execute twice
927
+ User.create!(email: event.payload.email)
928
+ end
929
+ ```
930
+
931
+ ### Real-World Consuming Examples
932
+
933
+ #### Processing in Rake Task
934
+
935
+ ```ruby
936
+ # lib/tasks/consume_events.rake
937
+ namespace :jetstream do
938
+ desc "Start event consumer"
939
+ task consume: :environment do
940
+ consumer = JetstreamBridge.subscribe do |event|
941
+ Rails.logger.info "Processing #{event.type} (attempt #{event.deliveries})"
942
+
943
+ case event.resource_type
944
+ when "user"
945
+ UserEventHandler.process(event)
946
+ when "order"
947
+ OrderEventHandler.process(event)
948
+ else
949
+ Rails.logger.warn "Unknown resource type: #{event.resource_type}"
950
+ end
951
+ end
952
+
953
+ # Graceful shutdown on SIGTERM
954
+ trap("TERM") { consumer.stop! }
955
+
956
+ consumer.run!
957
+ end
958
+ end
959
+ ```
960
+
961
+ #### Background Job Processing
962
+
963
+ ```ruby
964
+ # Offload to background jobs for complex processing
965
+ JetstreamBridge.subscribe(run: true) do |event|
966
+ ProcessEventJob.perform_later(
967
+ event: event.to_h.to_json,
968
+ event_id: event.event_id,
969
+ trace_id: event.trace_id
970
+ )
971
+ end
972
+
973
+ # app/jobs/process_event_job.rb
974
+ class ProcessEventJob < ApplicationJob
975
+ queue_as :events
976
+
977
+ def perform(event:, event_id:, trace_id:)
978
+ event_data = JSON.parse(event)
979
+
980
+ # Complex processing with retries
981
+ case event_data["type"]
982
+ when "user.created"
983
+ SendWelcomeEmailService.call(event_data["payload"])
984
+ when "order.completed"
985
+ GenerateInvoiceService.call(event_data["payload"])
986
+ end
987
+ rescue => e
988
+ logger.error "Failed to process event #{event_id}: #{e.message}"
989
+ raise # Let Sidekiq retry
990
+ end
991
+ end
281
992
  ```
282
993
 
283
- `durable_name` and `batch_size` default to the configured values and can be
284
- overridden if needed:
994
+ #### Event Router Pattern
285
995
 
286
996
  ```ruby
287
- JetstreamBridge::Consumer.new(durable_name: 'my-durable', batch_size: 10) do |event, subject, deliveries|
288
- # ...
997
+ # app/services/event_router.rb
998
+ class EventRouter
999
+ def self.route(event)
1000
+ handler_class = "#{event.resource_type.camelize}#{event.type.camelize}Handler"
1001
+
1002
+ if Object.const_defined?(handler_class)
1003
+ handler_class.constantize.new.call(event)
1004
+ else
1005
+ Rails.logger.warn "No handler for #{event.resource_type}.#{event.type}"
1006
+ end
1007
+ end
1008
+ end
1009
+
1010
+ # Start consumer with router
1011
+ JetstreamBridge.subscribe do |event|
1012
+ EventRouter.route(event)
289
1013
  end.run!
290
1014
  ```
291
1015
 
292
- If **Inbox** is enabled, the consumer:
1016
+ #### Multi-Service Consumer
1017
+
1018
+ ```ruby
1019
+ # app/consumers/application_consumer.rb
1020
+ class ApplicationConsumer
1021
+ def self.start!
1022
+ consumer = JetstreamBridge.subscribe do |event|
1023
+ begin
1024
+ new.process(event)
1025
+ rescue => e
1026
+ Rails.logger.error "Error processing event: #{e.message}"
1027
+ raise # Trigger retry/DLQ
1028
+ end
1029
+ end
1030
+
1031
+ # Handle signals gracefully
1032
+ %w[INT TERM].each do |signal|
1033
+ trap(signal) do
1034
+ Rails.logger.info "Shutting down consumer..."
1035
+ consumer.stop!
1036
+ exit
1037
+ end
1038
+ end
1039
+
1040
+ Rails.logger.info "Consumer started. Press Ctrl+C to stop."
1041
+ consumer.run!
1042
+ end
1043
+
1044
+ def process(event)
1045
+ # Log for observability
1046
+ Rails.logger.info(
1047
+ message: "Processing event",
1048
+ event_id: event.event_id,
1049
+ event_type: event.type,
1050
+ resource_type: event.resource_type,
1051
+ trace_id: event.trace_id,
1052
+ subject: event.subject,
1053
+ deliveries: event.deliveries
1054
+ )
1055
+
1056
+ # Route to specific handler
1057
+ handler = handler_for(event)
1058
+ handler.call(event.payload.to_h, event)
1059
+ end
1060
+
1061
+ private
1062
+
1063
+ def handler_for(event)
1064
+ case [event.resource_type, event.type]
1065
+ when ["user", "created"]
1066
+ UserCreatedHandler
1067
+ when ["user", "updated"]
1068
+ UserUpdatedHandler
1069
+ when ["order", "completed"]
1070
+ OrderCompletedHandler
1071
+ else
1072
+ UnknownEventHandler
1073
+ end
1074
+ end
1075
+ end
1076
+ ```
1077
+
1078
+ #### Dockerized Consumer
1079
+
1080
+ ```dockerfile
1081
+ # Dockerfile.consumer
1082
+ FROM ruby:3.2
293
1083
 
294
- * Dedupes by `event_id` (falls back to stream sequence if needed)
295
- * Records processing state, errors, and timestamps
296
- * Skips already-processed messages (acks immediately)
1084
+ WORKDIR /app
1085
+ COPY . .
1086
+ RUN bundle install
1087
+
1088
+ CMD ["bundle", "exec", "rake", "jetstream:consume"]
1089
+ ```
1090
+
1091
+ ```yaml
1092
+ # docker-compose.yml
1093
+ services:
1094
+ consumer:
1095
+ build:
1096
+ context: .
1097
+ dockerfile: Dockerfile.consumer
1098
+ environment:
1099
+ - NATS_URLS=nats://nats:4222
1100
+ - RAILS_ENV=production
1101
+ depends_on:
1102
+ - nats
1103
+ restart: unless-stopped
1104
+ ```
297
1105
 
298
1106
  ---
299
1107
 
@@ -319,10 +1127,97 @@ If **Inbox** is enabled, the consumer:
319
1127
 
320
1128
  ## 🧨 Dead-Letter Queue (DLQ)
321
1129
 
322
- When enabled, the topology ensures the DLQ subject exists:
323
- **`{env}.sync.dlq`**
1130
+ When enabled, the topology ensures each app has its own DLQ subject:
1131
+ **`{env}.{app_name}.sync.dlq`**
1132
+
1133
+ This per-app DLQ approach provides:
1134
+
1135
+ * **Isolation**: Failed messages from different services don't mix
1136
+ * **Easier Monitoring**: Track DLQ metrics per service
1137
+ * **Simpler Debugging**: Identify which service is having issues
1138
+ * **Independent Processing**: Each team can manage their own DLQ consumer
1139
+
1140
+ ### How DLQ Works
1141
+
1142
+ Messages are automatically moved to the DLQ when:
1143
+
1144
+ * Delivery attempts exceed `max_deliver` (default: 5)
1145
+ * Handler raises an unrecoverable error
1146
+ * Message cannot be processed successfully after all retries
1147
+
1148
+ ### Consuming DLQ Messages
1149
+
1150
+ You can run a separate consumer to monitor and process DLQ messages:
1151
+
1152
+ ```ruby
1153
+ # lib/tasks/dlq_consumer.rake
1154
+ namespace :jetstream do
1155
+ desc "Process Dead Letter Queue messages"
1156
+ task consume_dlq: :environment do
1157
+ # Create a custom consumer for DLQ subject
1158
+ dlq_subject = JetstreamBridge.config.dlq_subject
1159
+
1160
+ consumer = JetstreamBridge::Consumer.new(
1161
+ durable_name: "#{JetstreamBridge.config.env}-dlq-processor"
1162
+ ) do |event|
1163
+ # Log failed event for manual review
1164
+ Rails.logger.error(
1165
+ "DLQ Event: #{event.event_id}",
1166
+ event_type: event.type,
1167
+ deliveries: event.deliveries,
1168
+ payload: event.payload.to_h,
1169
+ trace_id: event.trace_id
1170
+ )
1171
+
1172
+ # Optionally: store in database for manual intervention
1173
+ FailedEvent.create!(
1174
+ event_id: event.event_id,
1175
+ event_type: event.type,
1176
+ payload: event.payload.to_h,
1177
+ deliveries: event.deliveries,
1178
+ failed_at: Time.current
1179
+ )
1180
+
1181
+ # Or attempt recovery logic
1182
+ case event.type
1183
+ when "payment.processed"
1184
+ # Manual payment reconciliation
1185
+ PaymentReconciliationService.call(event.payload.to_h)
1186
+ else
1187
+ # Alert on-call team
1188
+ AlertService.notify("DLQ event requires attention", event: event.to_h)
1189
+ end
1190
+ end
1191
+
1192
+ # Graceful shutdown
1193
+ trap("TERM") { consumer.stop! }
1194
+
1195
+ Rails.logger.info "DLQ Consumer started on #{dlq_subject}"
1196
+ consumer.run!
1197
+ end
1198
+ end
1199
+ ```
1200
+
1201
+ **Run the DLQ consumer:**
1202
+
1203
+ ```bash
1204
+ bundle exec rake jetstream:consume_dlq
1205
+ ```
1206
+
1207
+ ### DLQ Monitoring
1208
+
1209
+ Monitor DLQ health and volume:
1210
+
1211
+ ```ruby
1212
+ # Check DLQ message count
1213
+ stream_info = JetstreamBridge.stream_info
1214
+ dlq_count = stream_info[:messages] # Messages in DLQ subject
324
1215
 
325
- You may run a separate process to subscribe and triage messages that exceed `max_deliver` or are NAK'ed to the DLQ.
1216
+ # Alert if DLQ is growing
1217
+ if dlq_count > 100
1218
+ AlertService.notify("DLQ has #{dlq_count} messages")
1219
+ end
1220
+ ```
326
1221
 
327
1222
  ---
328
1223
 
@@ -331,8 +1226,9 @@ You may run a separate process to subscribe and triage messages that exceed `max
331
1226
  ### Monitoring
332
1227
 
333
1228
  * **Consumer lag**: `nats consumer info <stream> <durable>`
334
- * **DLQ volume**: subscribe/metrics on `{env}.sync.dlq`
335
- * **Outbox backlog**: alert on `jetstream_outbox_events` with `status != 'sent'` and growing count
1229
+ * **DLQ volume**: Monitor your app's DLQ subject `{env}.{app_name}.sync.dlq`
1230
+ * Example: `nats sub "production.api.sync.dlq" --count`
1231
+ * **Outbox backlog**: Alert on `jetstream_outbox_events` with `status != 'sent'` and growing count
336
1232
 
337
1233
  ### Scaling
338
1234
 
@@ -392,6 +1288,175 @@ JetstreamBridge::DebugHelper.debug_info
392
1288
 
393
1289
  ---
394
1290
 
1291
+ ## 🤝 Contributing
1292
+
1293
+ We welcome contributions from the community! Here's how you can help:
1294
+
1295
+ ### Getting Started
1296
+
1297
+ 1. **Fork the repository** on GitHub
1298
+ 2. **Clone your fork** locally:
1299
+
1300
+ ```bash
1301
+ git clone https://github.com/YOUR_USERNAME/jetstream_bridge.git
1302
+ cd jetstream_bridge
1303
+ ```
1304
+
1305
+ 3. **Install dependencies**:
1306
+
1307
+ ```bash
1308
+ bundle install
1309
+ ```
1310
+
1311
+ 4. **Set up NATS** for testing (requires Docker):
1312
+
1313
+ ```bash
1314
+ docker run -d -p 4222:4222 nats:latest -js
1315
+ ```
1316
+
1317
+ ### Development Workflow
1318
+
1319
+ 1. **Create a feature branch**:
1320
+
1321
+ ```bash
1322
+ git checkout -b feature/your-feature-name
1323
+ ```
1324
+
1325
+ 2. **Make your changes** with tests:
1326
+ * Write meaningful commit messages
1327
+ * Add tests for new functionality
1328
+ * Update documentation as needed
1329
+
1330
+ 3. **Run the test suite**:
1331
+
1332
+ ```bash
1333
+ bundle exec rspec
1334
+ ```
1335
+
1336
+ 4. **Check code quality**:
1337
+
1338
+ ```bash
1339
+ bundle exec rubocop
1340
+ ```
1341
+
1342
+ 5. **Push to your fork** and submit a pull request
1343
+
1344
+ ### Code Quality Standards
1345
+
1346
+ * **Test Coverage**: Maintain >80% line coverage and >70% branch coverage
1347
+ * **RuboCop**: All code must pass RuboCop checks with zero offenses
1348
+ * **Tests**: All tests must pass before merging
1349
+ * **Documentation**: Update README and inline docs for new features
1350
+
1351
+ ### Pull Request Guidelines
1352
+
1353
+ * **Title**: Use clear, descriptive titles (e.g., "Add health check endpoint generator")
1354
+ * **Description**: Explain what changes were made and why
1355
+ * **Tests**: Include tests for bug fixes and new features
1356
+ * **Documentation**: Update relevant documentation
1357
+ * **One feature per PR**: Keep pull requests focused and reviewable
1358
+
1359
+ ### Reporting Issues
1360
+
1361
+ When reporting bugs, please include:
1362
+
1363
+ * **Ruby version**: Output of `ruby -v`
1364
+ * **Gem version**: Output of `bundle show jetstream_bridge`
1365
+ * **NATS version**: Version of NATS server
1366
+ * **Steps to reproduce**: Minimal example that reproduces the issue
1367
+ * **Expected behavior**: What you expected to happen
1368
+ * **Actual behavior**: What actually happened
1369
+ * **Logs/errors**: Relevant error messages or stack traces
1370
+
1371
+ ### Feature Requests
1372
+
1373
+ We love hearing your ideas! When proposing features:
1374
+
1375
+ * Search existing issues to avoid duplicates
1376
+ * Describe the problem you're trying to solve
1377
+ * Explain your proposed solution
1378
+ * Consider backwards compatibility
1379
+
1380
+ ## 🏗️ Development
1381
+
1382
+ ### Running Tests
1383
+
1384
+ ```bash
1385
+ # Run all tests
1386
+ bundle exec rspec
1387
+
1388
+ # Run specific test file
1389
+ bundle exec rspec spec/publisher/publisher_spec.rb
1390
+
1391
+ # Run with coverage report
1392
+ COVERAGE=true bundle exec rspec
1393
+ ```
1394
+
1395
+ ### Code Coverage
1396
+
1397
+ Coverage reports are generated automatically and saved to `coverage/`. View the HTML report:
1398
+
1399
+ ```bash
1400
+ open coverage/index.html
1401
+ ```
1402
+
1403
+ ### Linting
1404
+
1405
+ ```bash
1406
+ # Run RuboCop
1407
+ bundle exec rubocop
1408
+
1409
+ # Auto-fix violations
1410
+ bundle exec rubocop -A
1411
+ ```
1412
+
1413
+ ### Local Testing with Rails
1414
+
1415
+ To test the gem in a Rails application:
1416
+
1417
+ 1. Point your Gemfile to the local path:
1418
+
1419
+ ```ruby
1420
+ gem "jetstream_bridge", path: "../jetstream_bridge"
1421
+ ```
1422
+
1423
+ 2. Run bundle:
1424
+
1425
+ ```bash
1426
+ bundle install
1427
+ ```
1428
+
1429
+ ## 📋 Code of Conduct
1430
+
1431
+ This project follows the [Contributor Covenant Code of Conduct](CODE_OF_CONDUCT.md). By participating, you are expected to uphold this code. Please report unacceptable behavior to the project maintainers.
1432
+
1433
+ ### Our Standards
1434
+
1435
+ * **Be respectful**: Treat everyone with respect and consideration
1436
+ * **Be inclusive**: Welcome diverse perspectives and experiences
1437
+ * **Be collaborative**: Work together constructively
1438
+ * **Be professional**: Keep discussions focused and constructive
1439
+
1440
+ ## 🌟 Community
1441
+
1442
+ * **Discussions**: Use GitHub Discussions for questions and ideas
1443
+ * **Issues**: Report bugs and request features via GitHub Issues
1444
+ * **Pull Requests**: Submit improvements via pull requests
1445
+
1446
+ ## 🙏 Acknowledgments
1447
+
1448
+ Built with:
1449
+
1450
+ * [NATS.io](https://nats.io) - High-performance messaging system
1451
+ * [nats-pure.rb](https://github.com/nats-io/nats-pure.rb) - Ruby client for NATS
1452
+
1453
+ ## 📊 Project Status
1454
+
1455
+ * **CI/CD**: Automated testing and code quality checks
1456
+ * **Code Coverage**: 85%+ maintained
1457
+ * **Active Development**: Regular updates and maintenance
1458
+ * **Semantic Versioning**: Follows [SemVer](https://semver.org/)
1459
+
395
1460
  ## 📄 License
396
1461
 
397
- [MIT License](LICENSE)
1462
+ [MIT License](LICENSE) - Copyright (c) 2025 Mike Attara