Skip to content

Commit

Permalink
upd hotwire and serverless docs
Browse files Browse the repository at this point in the history
  • Loading branch information
palkan committed Apr 1, 2024
1 parent 434e97a commit 9346b6f
Show file tree
Hide file tree
Showing 2 changed files with 89 additions and 213 deletions.
204 changes: 16 additions & 188 deletions docs/guides/hotwire.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,134 +6,49 @@ AnyCable be used as a [Turbo Streams][] backend for **any application**, not onl

Since [turbo-rails][] uses Action Cable under the hood, no additional configuration is required to use AnyCable with Hotwired Rails applications. See the [getting started guide](../rails/getting_started.md) for instructions.

However, you can squeeze even more _power_ from AnyCable for Hotwire apps by going the **RPC-less way**.
We recommend using AnyCable in a _standalone mode_ (i.e., without running an [RPC server](../anycable-go/rpc.md)) for applications only using Action Cable for Turbo Streams. For that, you must accomplish the following steps:

### RPC-less setup for Rails
- Generate AnyCable **application secret** for your application and store it in the credentials (`anycable.secret`) or the environment variable (`ANYCABLE_SECRET`).

> 🎥 Check out this AnyCasts screencast—it's a video guide on setting up Hotwire with AnyCable in the RPC-less way: [Exploring Rails 7, Hotwire and AnyCable speedy streams](https://anycable.io/blog/anycasts-rails-7-hotwire-and-anycable/).
- Enable JWT authentication by using the `action_cable_with_jwt_meta_tag(**identifiers)` helper instead of the `action_cable_meta_tag` (see [docs](../rails/authentication.md)).

The following steps are required to go the RPC-less way with a Rails application:

- Install and configure the [anycable-rails-jwt][] gem:

```yml
# anycable.yml
production:
jwt_id_key: "some-secret-key"
```
- Configure the Turbo Streams verifier key:
- Configure Turbo to use your AnyCable application secret for signing streams:

```ruby
# config/environments/production.rb
config.turbo.signed_stream_verifier_key = "s3cЯeT"
config.turbo.signed_stream_verifier_key = "<your-secret>"
```

- Enable JWT identification and signed streams in AnyCable-Go:
- Enable Turbo Streams support for AnyCable server:

```sh
ANYCABLE_JWT_ID_KEY=some-secret-key \
ANYCABLE_TURBO_RAILS_KEY=s3cЯeT \
ANYCABLE_SECRET=your-secret \
ANYCABLE_TURBO_STREAMS=true \
anycable-go
# or via cli args
anycable-go --jwt_id_key=some-secret-key --turbo_rails_key=s3cЯeT
anycable-go --secret=your-secret --turbo_streams
```

That's it! Now you Turbo Stream connections are served solely by AnyCable-Go.
That's it! Now you Turbo Stream connections are served solely by AnyCable server.
## Other frameworks and languages
Hotwire is not limited to Ruby on Rails. You can use Turbo with any backend. Live updates via Turbo Streams, however, require a _connection_ to receive the updates. This is where AnyCable comes into play.
You can use AnyCable-Go as a WebSocket server for Turbo Streams if you enable [Turbo signed streams](../anycable-go/signed_streams.md) and [JWT authentication](../anycable-go/jwt_identification.md) features.

The complete setup looks as follows:

- Implement JWT token generation.

Here is an example Ruby code:

```ruby
# Connection identifiers can be used to distinguish users
identifiers = {user_id: 42}
# Expiration is optional
payload = {ext: identifiers.to_json, exp: Time.now.to_i + 300}
JWT.encode payload, ENCRYPTION_KEY, "HS256"
```

The Python version would look like this:

```python
import json
import jwt
import time
identifiers = {'user_id': 42}
payload = {'ext': json.dumps(identifiers), 'exp': int(time.time()) + 300}
jwt.encode(payload, ENCRYPTION_KEY, algorithm='HS256')
```

The PHP version is as follows:
You can use AnyCable as a real-time server for Turbo Streams as follows:
```php
use Firebase\JWT\JWT;
$identifiers = ['user_id' => 42];
$payload = ['ext' => json_encode($identifiers), 'exp' => time() + 300];
$jwt = JWT::encode($payload, $ENCRYPTION_KEY, 'HS256');
```
- Use [JWT authentication](../anycable-go/jwt_identification.md) to authenticate conenctions (or run your AnyCable server with authentication disabled via the `--noauth` option)
- Implement stream signing\*:
- Enable [Turbo signed streams](../anycable-go/signed_streams.md#hotwire-and-cableready-support) support.
Here is the Ruby code to sign streams the same way as Rails does:

```ruby
encoded = ::Base64.strict_encode64(JSON.dump(stream_name))
digest = OpenSSL::HMAC.hexdigest("SHA256", SECRET_KEY, encoded)
signed_stream_name = "#{encoded}--#{digest}"
```
- Configure your backend to broadcast Turbo Streams updates via AnyCable (see [broadcasting documentation](../anycable-go/broadcasting.md)).
The Python version looks as follows:

```python
import base64
import json
import hmac
import hashlib
encoded = base64.b64encode(json.dumps(stream_name).encode('utf-8')).decode('utf-8')
digest = hmac.new(SECRET_KEY.encode('utf-8'), encoded.encode('utf-8'), hashlib.sha256).hexdigest()
signed_stream_name = f"{encoded}--{digest}"
```

The PHP version is as follows:

```php
$encoded = base64_encode(json_encode($stream_name));
$digest = hash_hmac('sha256', $encoded, $SECRET_KEY);
$signed_stream_name = $encoded . '--' . $digest;
```

- Enable JWT identification and signed streams in AnyCable-Go and use the [HTTP broadcast adapter](../ruby/broadcast_adapters.md#http-adapter):

```sh
ANYCABLE_JWT_ID_KEY=$ENCRYPTION_KEY \
ANYCABLE_TURBO_RAILS_KEY=$SECRET_KEY \
ANYCABLE_BROADCAST_ADAPTER=http \
anycable-go
# or via cli args
anycable-go --jwt_id_key=$ENCRYPTION_KEY --turbo_rails_key=$SECRET_KEY --broadcast_adapter=http
```

- You can use either the official `@hotwired/turbo-rails` package or [@anycable/turbo-stream][] package at the client side.

\* It's possible to use unsigned stream names, too. For that, you need to specify the additional option when running AnyCable-Go: `--turbo_rails_cleartext`. This way, you don't need to implement stream signing and rely only on JWT for authentication.
With this setup, you can use `@hotwired/turbo-rails` or [@anycable/turbo-stream][] JavaScript libraries in your application without any modification.
## Turbo Streams over Server-Sent Events
AnyCable-Go supports [Server-Sent Events](../anycable-go/sse.md) (SSE) as a transport protocol. This means that you can use Turbo Streams with AnyCable-Go without WebSockets and Action Cable (or AnyCable) client libraries—just with the help of the browser native `EventSource` API.
AnyCable supports [Server-Sent Events](../anycable-go/sse.md) (SSE) as a transport protocol. This means that you can use Turbo Streams with AnyCable without WebSockets and Action Cable (or AnyCable) client libraries—just with the help of the browser native `EventSource` API.
To create a Turbo Stream subscription over SSE, you must provide an URL to AnyCable SSE endpoint with the signed stream name as a query parameter when adding a `<turbo-stream-source>` element on the page:
Expand All @@ -143,93 +58,6 @@ To create a Turbo Stream subscription over SSE, you must provide an URL to AnyCa
That's it! Now you can broadcast Turbo Stream updates from your backend. Moreover, AnyCable supports the `Last-Event-ID` feature of EventSource, which means your **connection is reliable** and you won't miss any updates even if network is unstable. Don't forget to enable the [reliable streams](../anycable-go/reliable_streams.md) feature.

**NOTE:** If you use unsigned streams (`--turbo_rails_cleartext`), you should pass the plain stream name as a query parameter, e.g. `?turbo_stream_name=chat_42`.

## RPC-less setup in detail

> 📖 See also [JWT identification and “hot streams”](https://anycable.io/blog/jwt-identification-and-hot-streams/).

AnyCable-Go provides a feature called [signed streams](../anycable-go/signed_streams.md), which implements the require `turbo-rails` Action Cable functionality right in the WebSocket server. This means that subscribing to Turbo Streams doesn't require calling a gRPC Rails server.

If you're only using Turbo Streams and don't rely on _pure_ Action Cable, you can simplify your AnyCable configuration (infrastructure, deployment) by switching to signed streams and [JWT authentication](../anycable-go/jwt_identification.md).

**What's the point?** Here are the main benefits of going the RPC-less way:

- Improve application performance by speeding up WebSocket handshakes and commands.
- Reduce infrastructure burden by removing the need to run a separate service (RPC). Especially helpful on [Heroku](../deployment/heroku.md).
- Open the possibility of using Turbo Streams without Rails and even Ruby!

The default flow with AnyCable RPC looks like this:

```mermaid
sequenceDiagram
participant Rails
participant AnyCableRPC
participant AnyCableGo
participant Client
autonumber
Rails-->>Client: action_cable_meta_tag
Client->>AnyCableGo: HTTP Connect
activate AnyCableGo
AnyCableGo->>AnyCableRPC: gRPC Connect
activate AnyCableRPC
note left of AnyCableRPC: ApplicationCable::Connection#35;connect
AnyCableRPC->>AnyCableGo: Status OK
deactivate AnyCableRPC
AnyCableGo->>Client: Welcome
deactivate AnyCableGo
Rails-->>Client: turbo_stream_from signed_stream_id
Client->>AnyCableGo: Subscribe to signed_stream_id
activate AnyCableGo
AnyCableGo-->>AnyCableRPC: gRPC Command subscribe
activate AnyCableRPC
note left of AnyCableRPC: Turbo::StreamChannel#35;subscribe
AnyCableRPC->>AnyCableGo: Status OK
deactivate AnyCableRPC
AnyCableGo->>Client: Subscription Confirmation
deactivate AnyCableGo
loop
Rails--)AnyCableGo: ActionCable.server.broadcast
AnyCableGo->>Client: Deliver <turbo-stream>
end
```

Compare this with the RPC-less configuration which has the aforementioned features:

```mermaid
sequenceDiagram
participant Rails
participant AnyCableGo
participant Client
autonumber
Rails-->>Client: action_cable_meta_tag_with_jwt(user: current_user)
Client->>AnyCableGo: HTTP Connect
activate AnyCableGo
note left of AnyCableGo: Verify JWT and store identifiers
AnyCableGo->>Client: Welcome
deactivate AnyCableGo
Rails-->>Client: turbo_stream_from signed_stream_id
Client->>AnyCableGo: Subscribe to signed_stream_id
activate AnyCableGo
note left of AnyCableGo: Verify Signed Stream ID and Subscribe
AnyCableGo->>Client: Subscription Confirmation
deactivate AnyCableGo
loop
Rails--)AnyCableGo: ActionCable.server.broadcast
AnyCableGo->>Client: Deliver <turbo-stream>
end
```

[Hotwire]: https://hotwired.dev
[anycable-rails-jwt]: https://github.com/anycable/anycable-rails-jwt
[Turbo Streams]: https://turbo.hotwired.dev/handbook/streams
[turbo-rails]: https://github.com/hotwired/turbo-rails
[@anycable/turbo-stream]: https://github.com/anycable/anycable-client/tree/master/packages/turbo-stream
98 changes: 73 additions & 25 deletions docs/guides/serverless.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,27 +6,86 @@ AnyCable is a great companion for your serverless JavaScript (and TypeScript) ap

To use AnyCable with a serverless JS application, you need to:

- Deploy AnyCable-Go to a platform of your choice (see [below](#deploying-anycable-go)).
- Deploy AnyCable server to a platform of your choice (see [below](#deploying-anycable)).
- Configure AnyCable API handler in your JS application.
- Use [AnyCable Client SDK][anycable-client] to communicate with the AnyCable server from your client.

<picture class="captioned-figure">
<source srcset="/assets/serverless-dark.png" media="(prefers-color-scheme: dark)">
<img align="center" alt="AnyCable + Node.js serverless architecture" style="max-width:80%" title="AnyCable + Node.js serverless architecture" src="/assets/serverless-light.png">
</picture>

AnyCable-Go will handle WebSocket connections and translate incoming commands into API calls to your serverless functions, where you can manage subscriptions and respond to commands.
AnyCable will handle WebSocket/SSE connections and translate incoming commands into API calls to your serverless functions, where you can manage subscriptions and respond to commands.

Broadcasting real-time updates is as easy as performing POST requests to AnyCable-Go.
Broadcasting real-time updates is as easy as performing POST requests to AnyCable.

Luckily, you don't need to write all this code from scratch. Our JS SDK makes it easy to integrate AnyCable with your serverless application.

### Standalone real-time server

You can run AnyCable in a standalone mode by using [signed pub/sub streams](../anycable-go/signed_streams.md) and [JWT authentication](../anycable-go/jwt_identification.md). In this case, all real-time actions are pre-authorized and no API handlers are required.

> Check out this Next.js demo chat application running fully within Stackblitz and backed by AnyCable pub/sub streams: [![Open in StackBlitz](https://developer.stackblitz.com/img/open_in_stackblitz.svg)](https://stackblitz.com/edit/anycable-pubsub?file=README.md)
## AnyCable Serverless SDK

[AnyCable Serverless SDK][anycable-serverless-js] is a Node.js package that provides a set of helpers to integrate AnyCable with your serverless application.
[AnyCable Serverless SDK][anycable-serverless-js] is a Node.js package that provides a set of helpers to integrate AnyCable into your JavaScript backend application.

> Check out our demo Next.js application to see the complete example: [vercel-anycable-demo][]
AnyCable SDK uses _channels_ to encapsulate real-time logic. For example, a channel representing a chat room may be defined as follows:
AnyCable Serverless SDK contains the following components:

- JWT authentication and signed streams.
- Broadcasting.
- Channels.

### JWT authentication

AnyCable support [JWT-based authentication](../anycable-go/jwt_identification.md). With the SDK, you can generate tokens as follows:

```js
import { identificator } from "@anycable/serverless-js";

const jwtSecret = "very-secret";
const jwtTTL = "1h";

export const identifier = identificator(jwtSecret, jwtTTL);

// Then, somewhere in your code, generate a token and provide it to the client
const userId = authenticatedUser.id;
const token = await identifier.generateToken({ userId });
```

### Signed streams

_🛠️ Coming soon_. Check out the [signed streams documentation](../anycable-go/signed_streams.md)_.

### Broadcasting

SDK provides utilities to publish messages to AnyCable streams via HTTP:

```js
import { broadcaster } from "@anycable/serverless-js";

// Broadcasting configuration
const broadcastURL =
process.env.ANYCABLE_BROADCAST_URL || "http://127.0.0.1:8090/_broadcast";
const broadcastKey = process.env.ANYCABLE_BROADCAST_KEY || "";

// Create a broadcasting function to send broadcast messages via HTTP API
export const broadcastTo = broadcaster(broadcastURL, broadcastKey);

// Now, you can use the initialized broadcaster to publish messages
broadcastTo("chat/42", message);
```

Learn more about [broadcasting](../anycable-go/broadcasting.md).

### Channels

Channels help to encapsulate your real-time logic and enhance typical pub/sub capabilities with the ability to handle incoming client commands.

For example, a channel representing a chat room may be defined as follows:

```js
import { Channel } from "@anycable/serverless-js";
Expand Down Expand Up @@ -67,7 +126,7 @@ export default class ChatChannel extends {
}
```
Channels are registered within an _application_ instance, which is also responsible for authenticating connections (before they are subscribed to channels):
Channels are registered within an _application_ instance, which can also be used to authenticate connections (if JWT is not being used):
```js
import { Application } from "@anycable/serverless-js";
Expand Down Expand Up @@ -110,21 +169,9 @@ const app = new CableApplication();
app.register("chat", ChatChannel);
```
Finally, SDK provides utilities to publish messages to streams:
```js
import { broadcaster } from "@anycable/serverless-js";
// Broadcasting configuration
const broadcastURL =
process.env.ANYCABLE_BROADCAST_URL || "http://127.0.0.1:8090/_broadcast";
const broadcastToken = process.env.ANYCABLE_HTTP_BROADCAST_SECRET || "";
// Create a broadcasting function to send broadcast messages via HTTP API
export const broadcastTo = broadcaster(broadcastURL, broadcastToken);
```
To connect your channels to an AnyCable server, you MUST add AnyCable API endpoint to your HTTP server (or serverless function). The SDK provides HTTP handlers for that.
The final step is to set up an HTTP handler to process AnyCable requests and translate them into channel actions. Here is, for example, how you can do this with Next.js via [Vercel serverless functions](https://vercel.com/docs/functions/serverless-functions):
Here is an example setup for Next.js via [Vercel serverless functions](https://vercel.com/docs/functions/serverless-functions):
```js
// api/anycable/route.ts
Expand Down Expand Up @@ -182,20 +229,21 @@ channel.sendMessage({ body: "Hello, world!" });
**NOTE:** Both serverless and client SDKs support TypeScript so that you can leverage the power of static typing in your real-time application.
## Deploying AnyCable-Go
## Deploying AnyCable
AnyCable-Go can be deployed anywhere from modern clouds to good old bare-metal servers. Check out the [deployment guide](../deployment.md) for more details.
> The quickest way to get AnyCable is to use our managed (and free) solution: [plus.anycable.io](https://plus.anycable.io)
As the quickest option, we recommend using [Fly][]. You can deploy AnyCable-Go in a few minutes using a single command:
AnyCable can be deployed anywhere from modern clouds to good old bare-metal servers. Check out the [deployment guide](../deployment.md) for more details. We recommend using [Fly][], as you can deploy AnyCable in a few minutes with just a single command:
```sh
fly launch --image anycable/anycable-go:1 --generate-name --ha=false \
fly launch --image anycable/anycable-go:1.5 --generate-name --ha=false \
--internal-port 8080 --env PORT=8080 \
--env ANYCABLE_SECERT=<YOUR_SECRET> \
--env ANYCABLE_PRESETS=fly,broker \
--env ANYCABLE_RPC_HOST=https://<YOUR_JS_APP_HOSTNAME>/api/anycable
```
## Running AnyCable-Go locally
## Running AnyCable locally
There are plenty of ways of installing `anycable-go` binary on your machine (see [../anycable-go/getting_started.md]). For your convenience, we also provide an NPM package that can be used to install and run `anycable-go`:
Expand Down

0 comments on commit 9346b6f

Please sign in to comment.