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.