github.com/anycable/anycable-go@v1.5.1/docs/signed_streams.md (about)

     1  # Signed streams
     2  
     3  AnyCable allows you to subscribe to _streams_ without using _channels_ (in Action Cable terminology). Channels is a great way to encapsulate business-logic for a given real-time feature, but in many cases all we need is a good old explicit pub/sub. That's where the **signed streams** feature comes into play.
     4  
     5  > You read more about the Action Cable abstract design, how it compares to direct pub/sub and what are the pros and cons from this [Any Cables Monthly issue](https://anycable.substack.com/p/any-cables-monthly-18). Don't forget to subscribe!
     6  
     7  Signed streams work as follows:
     8  
     9  - Given a stream name, say, "chat/2024", you generate its signed version using a **secret key** (see below on the signing algorithm)
    10  
    11  - On the client side, you subscribe to the "$pubsub" channel and provide the signed stream name as a `signed_stream_name` parameter
    12  
    13  - AnyCable process the subscribe command, verifies the stream name and completes the subscription (if verified).
    14  
    15  For verification, you MUST provide the **secret key** via the `--streams_secret` (`ANYCABLE_STREAMS_SECRET`) parameter for AnyCable.
    16  
    17  ## Full-stack example: Rails
    18  
    19  Let's consider an example of using signed stream in a Rails application.
    20  
    21  Assume that we want to subscribe a user with ID=17 to their personal notifications channel, "notifications/17".
    22  
    23  First, we need to generate a signed stream name:
    24  
    25  ```ruby
    26  signed_name = AnyCable::Streams.signed("notifications/17")
    27  ```
    28  
    29  Or you can use the `#signed_stream_name` helper in your views
    30  
    31  ```erb
    32  <div
    33    data-controller="notifications"
    34    data-notifications-stream="<%= signed_stream_name("notifications/#{current_user.id}") %>">
    35  
    36  </div>
    37  ```
    38  
    39  By default, AnyCable uses `Rails.application.secret_key_base` to sign streams. We recommend configuring a custom secret though (so you can easily rotate values at both ends, the Rails app and AnyCable servers). You can specify it via the `streams_secret` configuration parameter (in `anycable.yml`, credentials, or environment).
    40  
    41  Then, on the client side, you can subscribe to this stream as follows:
    42  
    43  ```js
    44  // using @rails/actioncable
    45  let subscription = consumer.subscriptions.create(
    46    {channel: "$pubsub", signed_stream_name: stream},
    47    {
    48      received: (msg) => {
    49        // handle notification msg
    50      }
    51    }
    52  )
    53  
    54  // using @anycable/web
    55  let channel = cable.streamFromSigned(stream);
    56  channel.on("message", (msg) => {
    57    // handle notification
    58  })
    59  ```
    60  
    61  Now you can broadcast messages to this stream as usual:
    62  
    63  ```ruby
    64  ActionCable.server.broadcast "notifications/#{user.id}", payload
    65  ```
    66  
    67  ## Public (unsigned) streams
    68  
    69  Sometimes you may want to skip all the signing ceremony and use plain stream names instead. With AnyCable, you can do that by enabling the `--public_streams` option (or `ANYCABLE_PUBLIC_STREAMS=true`) for the AnyCable server:
    70  
    71  ```sh
    72  $ anycable-go --public_streams
    73  
    74  # or
    75  $ ANYCABLE_PUBLIC_STREAMS=true anycable-go
    76  ```
    77  
    78  With public streams enabled, you can subscribe to them as follows:
    79  
    80  ```js
    81  // using @rails/actioncable
    82  let subscription = consumer.subscriptions.create(
    83    {channel: "$pubsub", stream_name: "notifications/17"},
    84    {
    85      received: (msg) => {
    86        // handle notification msg
    87      }
    88    }
    89  )
    90  
    91  // using @anycable/web
    92  let channel = cable.streamFrom("notifications/17");
    93  channel.on("message", (msg) => {
    94    // handle notification
    95  })
    96  ```
    97  
    98  ## Signing algorithm
    99  
   100  We use the same algorithm as Rails uses in its [MessageVerifier](https://api.rubyonrails.org/v7.1.3/classes/ActiveSupport/MessageVerifier.html):
   101  
   102  1. Encode the stream name by first converting it into a JSON string and then encoding in Base64 format.
   103  1. Calculate a HMAC digest using the SHA256 hash function from the secret and the encoded stream name.
   104  1. Concatenate the encoded stream name, a double dash (`--`), and the digest.
   105  
   106  Here is the Ruby version of the algorithm:
   107  
   108  ```ruby
   109  encoded = ::Base64.strict_encode64(JSON.dump(stream_name))
   110  digest = OpenSSL::HMAC.hexdigest("SHA256", SECRET_KEY, encoded)
   111  signed_stream_name = "#{encoded}--#{digest}"
   112  ```
   113  
   114  The JavaScript (Node.js) version:
   115  
   116  ```js
   117  import { createHmac } from 'crypto';
   118  
   119  const encoded = Buffer.from(JSON.stringify(stream_name)).toString('base64');
   120  const digest = createHmac('sha256', SECRET_KEY).update(encoded).digest('hex');
   121  const signedStreamName = `${encoded}--${digest}`;
   122  ```
   123  
   124  The Python version looks as follows:
   125  
   126  ```python
   127  import base64
   128  import json
   129  import hmac
   130  import hashlib
   131  
   132  encoded = base64.b64encode(json.dumps(stream_name).encode('utf-8')).decode('utf-8')
   133  digest = hmac.new(SECRET_KEY.encode('utf-8'), encoded.encode('utf-8'), hashlib.sha256).hexdigest()
   134  signed_stream_name = f"{encoded}--{digest}"
   135  ```
   136  
   137  The PHP version is as follows:
   138  
   139  ```php
   140  $encoded = base64_encode(json_encode($stream_name));
   141  $digest = hash_hmac('sha256', $encoded, $SECRET_KEY);
   142  $signed_stream_name = $encoded . '--' . $digest;
   143  ```
   144  
   145  ## Whispering
   146  
   147  _Whispering_ is an ability to publish _transient_ broadcasts from clients, i.e., without touching your backend. This is useful when you want to share client-only information from one connection to others. Typical examples include typing indicators, cursor position sharing, etc.
   148  
   149  Whispering must be enabled explicitly for signed streams via the `--streams_whisper` (`ANYCABLE_STREAMS_WHISPER=true`) option. Public streams always allow whispering.
   150  
   151  Here is an example client code using AnyCable JS SDK:
   152  
   153  ```js
   154  let channel = cable.streamFrom("chat/22");
   155  
   156  channel.on("message", (msg) => {
   157    if (msg.event === "typing") {
   158      console.log(`user ${msg.name} is typing`);
   159    }
   160  })
   161  
   162  // publishing whispers
   163  channel.whisper({event: "typing", name: user.name})
   164  ```
   165  
   166  ## Hotwire and CableReady support
   167  
   168  AnyCable can be used to serve Hotwire ([Turbo Streams](https://turbo.hotwired.dev/handbook/streams)) and [CableReady](https://cableready.stimulusreflex.com) (v5+) subscriptions right at the real-time server using the same signed streams functionality under the hood (and, thus, without performing any RPC calls to authorize subscriptions).
   169  
   170  In combination with [JWT authentication](./jwt_identification.md), this feature makes it possible to run AnyCable in a standalone mode for Hotwire/CableReady applications.
   171  
   172  > 🎥 Check out this [AnyCasts episode](https://anycable.io/blog/anycasts-rails-7-hotwire-and-anycable/) to learn how to use AnyCable with Hotwire Rails application in a RPC-less way.
   173  
   174  You must explicitly enable Turbo Streams or CableReady signed streams support at the AnyCable server side by specifying the `--turbo_streams` (`ANYCABLE_TURBO_STREAMS=true`) or `--cable_ready_streams` (`ANYCABLE_CABLE_READY_STREAMS=true`) option respectively.
   175  
   176  You must also provide the `--streams_secret` corresponding to the secret you use for Turbo/CableReady. You can configure them in your Rails application as follows:
   177  
   178  ```ruby
   179  # Turbo configuration
   180  
   181  # config/environments/production.rb
   182  config.turbo.signed_stream_verifier_key = "<SECRET>"
   183  
   184  # CableReady configuration
   185  
   186  # config/initializers/cable_ready.rb
   187  CableReady.configure do |config|
   188    config.verifier_key = "<SECRET>"
   189  end
   190  ```
   191  
   192  You can also specify custom secrets for Turbo Streams and CableReady via the `--turbo_streams_secret` and `--cable_ready_secret` parameters respectively.