Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
9e4d805
release v1.13.1 changelog, documentation, and blog post
bensie May 18, 2026
21d6527
blog: clarify presence member events as pub/sub (SNS/EventBridge)
bensie May 18, 2026
43a2788
blog: drop expandReactions mention from intro
bensie May 18, 2026
c7e1479
blog: link all claims and directives
bensie May 18, 2026
5319d2a
blog: drop implementation detail about reaction counts
bensie May 18, 2026
42ee875
release v1.13.2 docs, skip v1.13.0 and v1.13.1
bensie May 18, 2026
986a3d2
blog: remove em-dashes
bensie May 18, 2026
5430381
blog: trim heartbeat tightening and Go version from other improvements
bensie May 18, 2026
a4fb63c
blog: drop other improvements section
bensie May 18, 2026
20ebf6c
homepage: surface v1.13 message reactions and presence pub/sub
bensie May 18, 2026
dbd7c14
remove inaccurate reaction pub/sub event docs
bensie May 18, 2026
6bcf32c
mark message reactions as preview
bensie May 18, 2026
feebc72
drop message reactions from v1.13 release notes
bensie May 18, 2026
1552b66
homepage: reorder capabilities to lead with private/presence, storage…
bensie May 18, 2026
4ece3d1
Add message reactions back to v1.13 release notes
bensie May 19, 2026
0075e2e
skip v1.13.2, point to v1.13.3 as the release
bensie May 20, 2026
2311b9a
changelog: collapse to v1.13.0, remove version skips
bensie May 20, 2026
2f02ff1
blog: update post date to May 19
bensie May 20, 2026
30a6d8e
changelog: drop DynamoDB stream processor implementation detail
bensie May 20, 2026
f54c024
drop listMessages limit parameter from docs (internal testing aid)
bensie May 20, 2026
5d4d818
changelog: note web console reaction support
bensie May 20, 2026
9a17e33
blog: use emoji for allowed reactions example
bensie May 20, 2026
5a80f6c
blog: use emoji in add/added reaction examples
bensie May 20, 2026
eb263ec
blog: use emoji consistently in all reaction examples
bensie May 20, 2026
ceaa6f6
blog: move TTL-delete detail to docs only
bensie May 20, 2026
0100b5b
events: fix reaction pub/sub docs to match actual EventBridge output
bensie May 20, 2026
f322284
events: note messageId is a ULID
bensie May 20, 2026
78c6d3b
events: use consistent sample data for reaction events
bensie May 20, 2026
25b2092
events: set hotsockVersion to 1.13.0 in all examples
bensie May 20, 2026
5c54a6f
events: duplicate metadata/data shape for messageReactionRemoved
bensie May 20, 2026
01fbaec
events: note removed metadata reflects the original add, not the removal
bensie May 20, 2026
455feda
events: use consistent uid/umd sample data across all pub/sub examples
bensie May 20, 2026
29cd5b7
events: use uid 12345 with first_name/last_name umd, keep member name…
bensie May 20, 2026
7528f65
events: use camelCase firstName/lastName in umd examples
bensie May 20, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
136 changes: 136 additions & 0 deletions blog/2026-05-19-new-in-v1.13.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
---
slug: message-reactions-and-presence-pubsub
title: Message reactions, presence pub/sub, and heartbeat enforcement
authors: [james]
tags: [releases, features, channels, presence]
description: Hotsock v1.13 adds message reactions with per-event permissions and real-time delivery, presence member events to SNS/EventBridge, and a heartbeatTimeout claim for server-enforced connection liveness.
---

Hotsock v1.13 introduces **message reactions**, **presence member events on pub/sub (SNS/EventBridge)**, and a **`heartbeatTimeout` claim** for server-enforced connection liveness.

{/* truncate */}

### Message reactions {/* #message-reactions */}

Clients can now add and remove reactions on stored messages, with real-time delivery to all channel subscribers, aggregated counts in message history, and a separate endpoint to list who reacted with what.

Reactions are gated by a new [`react`](/docs/connections/claims/#channels.messages.react) directive on channel message event patterns, alongside the existing [`publish`](/docs/connections/claims/#channels.messages.publish), [`store`](/docs/connections/claims/#channels.messages.store), [`echo`](/docs/connections/claims/#channels.messages.echo), and other directives. A connection needs three things to react: an active channel subscription, a [`uid`](/docs/connections/claims/#uid), and a stored target message that hasn't expired.

#### Setting up permissions {/* #setting-up-permissions */}

The `react` directive accepts `true` (allow any reaction), `false` (deny), or an array of strings to restrict which reactions are permitted:

```json
{
"exp": 1747574400,
"scope": "connect",
"uid": "Jim",
"channels": {
"room.123": {
"subscribe": true,
"messages": {
"chat": {
"publish": true,
"echo": true,
"store": 31536000,
// highlight-next-line
"react": ["👍", "❤️", "😂"]
}
}
}
}
}
```

#### Adding and removing reactions {/* #adding-and-removing-reactions */}

Clients send a `hotsock.messageReaction` message on the WebSocket with the target message ID, the reaction value, and an explicit `add` or `remove` action:

```
> {"event":"hotsock.messageReaction", "channel":"room.123", "data":{"messageId":"01JA3S0TNEC2QBYMZRBMCEXGNW", "reaction":"👍", "action":"add"}}
```

All channel subscribers immediately receive a `hotsock.messageReactionAdded` event:

```
< {"id":"01JB4K7M2N...","event":"hotsock.messageReactionAdded","channel":"room.123","data":{"messageId":"01JA3S0TNEC2QBYMZRBMCEXGNW","reaction":"👍","action":"add"},"meta":{"uid":"Jim","umd":null}}
```

Reaction data can be emoji characters, Slack-style colon-wrapped strings like `:thumbsup:`, or any short string up to 100 bytes. Each user can have one reaction per reaction value per message.

#### Reactions in message history {/* #reactions-in-message-history */}

[`listMessages`](/docs/connections/client-http-api/#connection/listMessages) responses now include a `reactions` object on each message that has reactions, keyed by reaction value:

```json
{
"messages": [
{
"id": "01JA3S0P7FB7WVYS67X316M32S",
"event": "chat",
"channel": "room.123",
"data": "Wow. Can we make it a different moment?",
"meta": { "uid": "Jim", "umd": null },
// highlight-next-line
"reactions": { "👍": { "count": 2 }, "❤️": { "count": 1 } }
}
]
}
```

Pass [`expandReactions: true`](/docs/connections/client-http-api/#connection/listMessages.expandReactions) to also include an `items` array per reaction with the user behind each one:

```json
"reactions": {
"👍": {
"count": 2,
"items": [
{ "uid": "Jim", "umd": { "name": "Jim Halpert" } },
{ "uid": "Pam" }
]
}
}
```

You can also fetch reactions for a single message with the dedicated [`connection/listReactions`](/docs/connections/client-http-api/#connection/listReactions) endpoint.

#### Pub/sub events for reactions {/* #pubsub-events-for-reactions */}

[`hotsock.messageReactionAdded`](/docs/server-api/events/#hotsock.messageReactionAdded) and [`hotsock.messageReactionRemoved`](/docs/server-api/events/#hotsock.messageReactionRemoved) events are emitted to SNS/EventBridge after reactions are persisted, gated by the [`emitPubSubEvent`](/docs/connections/claims/#channels.messages.emitPubSubEvent) directive on the matching message event pattern.

### Presence member events on pub/sub (SNS/EventBridge) {/* #presence-member-pubsub */}

Presence channel member lifecycle events ([`hotsock.memberAdded`](/docs/server-api/events/#hotsock.memberAdded), [`hotsock.memberRemoved`](/docs/server-api/events/#hotsock.memberRemoved), [`hotsock.memberUpdated`](/docs/server-api/events/#hotsock.memberUpdated)) are now also emitted to SNS/EventBridge, gated by the existing [`PublishEventsToSNSParameter` / `PublishEventsToEventBridgeParameter`](/docs/server-api/events/#eventbridge-sns-enable-disable) settings. The pub/sub events are deduplicated by [`uid`](/docs/connections/claims/#uid) to match WebSocket fan-out semantics, so joining from a second device when you're already present doesn't trigger another `memberAdded`.

The pub/sub `data` payload uses the same single-entity shape as the rest of the public event types, with [`uid`](/docs/connections/claims/#uid), [`umd`](/docs/connections/claims/#umd), and `members` at the root:

```json
{
"uid": "Dwight",
"umd": { "status": "available" },
"members": [
{ "uid": "Jim", "umd": null },
{ "uid": "Dwight", "umd": { "status": "available" } }
]
}
```

`dataType` is `channelMember`, so SNS filter policies and EventBridge rules can target presence events specifically.

### Server-enforced heartbeats {/* #server-enforced-heartbeats */}

The new [`heartbeatTimeout`](/docs/connections/claims/#heartbeatTimeout) connect-token claim sets a maximum interval between client-initiated [`hotsock.heartbeat`](/docs/connections/keep-alive/#send-hotsockheartbeat) messages. If the server doesn't see a heartbeat within that window, the connection is forcefully disconnected:

```json
{
"scope": "connect",
// highlight-next-line
"heartbeatTimeout": 30
}
```

Valid values are 5 to 600 seconds. This is most useful on [presence channels](/docs/channels/presence/) where accurate detection of dropped clients matters more than waiting for API Gateway's 10-minute idle timeout. The client just needs to send `{"event":"hotsock.heartbeat"}` periodically, and the server records receipt and reschedules the next check relative to the last heartbeat, with no reply.

### Wrapping up {/* #wrapping-up */}

Existing installations with auto-update enabled are already running v1.13 and have access to these features today. Other installations can be [manually updated](/docs/installation/updates/#manually-update-installation) at any time. A [full changelog](/docs/installation/changelog/#v1.13.0) is available with the complete list of changes included in this release.
91 changes: 90 additions & 1 deletion docs/connections/claims.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -226,7 +226,7 @@ If you wanted to allow access to the entire message history of a channel without

`Object` (optional) - Manages the permissions and directives for client-initiated messages that are published directly to the WebSocket on the channel(s) for the specified events. Each object key is the name of an event and can include asterisks (\*) anywhere in the string to denote wildcards. Keys can also use the `#regex:` prefix to specify a regular expression pattern for matching event names. Each object value is another object with the settings for that event or event pattern.

Each object inside the messages object accepts [`broadcast`](#channels.messages.broadcast), [`echo`](#channels.messages.echo), [`emitPubSubEvent`](#channels.messages.emitPubSubEvent), [`publish`](#channels.messages.publish), [`scheduleBefore`](#channels.messages.scheduleBefore), and [`store`](#channels.messages.store) attributes.
Each object inside the messages object accepts [`broadcast`](#channels.messages.broadcast), [`echo`](#channels.messages.echo), [`emitPubSubEvent`](#channels.messages.emitPubSubEvent), [`publish`](#channels.messages.publish), [`react`](#channels.messages.react), [`scheduleBefore`](#channels.messages.scheduleBefore), and [`store`](#channels.messages.store) attributes.

#### Regex patterns {/* #channels.messages--regex */}

Expand Down Expand Up @@ -405,6 +405,81 @@ In the following example, since `chat.*` includes `chat.admin`, this connection

:::

#### `react` {/* #channels.messages.react */}

`Boolean` or `Array[String]` (optional) - Controls whether this connection can add or remove reactions on stored messages that match this event pattern. Reactions require an active channel subscription and a [`uid`](#uid) on the connection. Default is `false`.

When set to `true`, any reaction value is permitted. When set to an array of strings, only reaction values in the list are allowed. An explicit `false` denies reactions.

When a reaction is added or removed, all channel subscribers receive a `hotsock.messageReactionAdded` or `hotsock.messageReactionRemoved` message in real-time. Reaction counts are included in [`listMessages`](./client-http-api.mdx#connection/listMessages) responses, and individual reactions can be listed with [`listReactions`](./client-http-api.mdx#connection/listReactions).

The following allows any reaction on `chat` messages in the "mychannel" channel:

```json
{
"channels": {
"mychannel": {
"subscribe": true,
"messages": {
"chat": {
"publish": true,
"echo": true,
"store": 31536000,
// highlight-next-line
"react": true
}
}
}
}
}
```

To restrict to specific reactions, pass an array:

```json
{
"channels": {
"mychannel": {
"subscribe": true,
"messages": {
"chat": {
"publish": true,
"store": 31536000,
// highlight-next-line
"react": ["thumbsup", "heart", "laugh"]
}
}
}
}
}
```

To add a reaction, send a `hotsock.messageReaction` message on the WebSocket:

```
> {"event":"hotsock.messageReaction", "channel":"mychannel", "data":{"messageId":"01JA3S0TNEC2QBYMZRBMCEXGNW", "reaction":"thumbsup", "action":"add"}}
```

To remove a reaction:

```
> {"event":"hotsock.messageReaction", "channel":"mychannel", "data":{"messageId":"01JA3S0TNEC2QBYMZRBMCEXGNW", "reaction":"thumbsup", "action":"remove"}}
```

All channel subscribers receive the reaction event:

```
< {"id":"01JB4K7M2N...","event":"hotsock.messageReactionAdded","channel":"mychannel","data":{"messageId":"01JA3S0TNEC2QBYMZRBMCEXGNW","reaction":"thumbsup","action":"add"},"meta":{"uid":"Jim","umd":null}}
```

Reaction data must not be blank, must be no more than 100 bytes, and must not contain number signs (`#`). Reactions can be emoji characters, Slack-style colon-wrapped strings like `:thumbsup:`, or any other short string.

:::info
Reactions can only be added to stored messages (messages published with a [`store`](#channels.messages.store) TTL). The target message must exist and not be expired. Each user can have one reaction per reaction value per message.
:::

Also available via the Client HTTP API at [`connection/publishMessage`](./client-http-api.mdx#connection/publishMessage) using `"event": "hotsock.messageReaction"` with the reaction payload as `data`, or via the server-side [Lambda](../server-api/publish-messages.mdx#publish-with-lambda) and [HTTP URL](../server-api/publish-messages.mdx#publish-with-http-url) publish APIs.

#### `scheduleBefore` {/* #channels.messages.scheduleBefore */}

`NumericDate` (optional) - If [client-initiated message publishing](#channels.messages.publish) is permitted for this channel and event, the `scheduleBefore` attribute specifies the furthest out future time that this connection can schedule messages for delivery. If supplied, the schedule before time must be expressed as a Unix timestamp - the number of seconds since the Unix epoch. By default, or if `scheduleBefore` is provided as `0` or any timestamp in the past, scheduled message publishing is not permitted.
Expand Down Expand Up @@ -747,6 +822,20 @@ You can also explicitly set `subscribe` to `false`, preventing subscriptions to
}
```

## `heartbeatTimeout` {/* #heartbeatTimeout */}

`Integer` (optional) - The maximum number of seconds between client-initiated heartbeats. If set, the connection must send a [`hotsock.heartbeat`](./keep-alive.mdx#send-hotsockheartbeat) event at least this often or it will be forcefully disconnected by the server. Must be between `5` and `600` (inclusive) if provided. Default behavior (when unset) is no heartbeat enforcement.

This is useful for [presence channels](../channels/presence.mdx) and other use cases where accurate detection of dropped clients matters more than the default API Gateway idle timeout would provide.

```json
{
"scope": "connect",
// highlight-next-line
"heartbeatTimeout": 30
}
```

## `iat` - Issued At {/* #iat */}

`NumericDate` (optional) - The issued at claim identifies the time when the JWT was issued, expressed as a Unix timestamp. Hotsock does not rely on this claim for authorization, but it's common for issuers to provide it by default which is why it's mentioned here.
Expand Down
98 changes: 96 additions & 2 deletions docs/connections/client-http-api.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ toc_max_heading_level: 4

# Client HTTP API

In addition to WebSocket interactions, connected clients can also make use of an HTTP API for publishing messages, listing message history, reading and writing channel storage, and updating user metadata. All requests **_must_ be `POST` requests** and **_must_ have URL-encoded `connectionId` and `connectionSecret`** query parameters set.
In addition to WebSocket interactions, connected clients can also make use of an HTTP API for publishing messages, listing message history, listing reactions, reading and writing channel storage, and updating user metadata. All requests **_must_ be `POST` requests** and **_must_ have URL-encoded `connectionId` and `connectionSecret`** query parameters set.

## Determine your API URL {/* #determine-your-api-url */}

Expand Down Expand Up @@ -59,6 +59,8 @@ When issuing a connect or subscribe token, specify the [`historyStart`](./claims

This endpoint will return up to 100 messages or up to 1MB of data, whichever comes first. Fetch subsequent pages using `before` or `after`, depending on your use case.

If any returned messages have reactions, a `reactions` object is included on each message, keyed by reaction value with `{count, items?}` per entry. By default only `count` is set. Pass [`expandReactions: true`](#connection/listMessages.expandReactions) to also populate `items` with one entry per reacting user.

#### `after` {/* #connection/listMessages.after */}

String (optional) - Load messages after (newer than) the message with the ID specified here. Has no effect if `reverse` is `true`.
Expand All @@ -79,6 +81,10 @@ Example: `01JA3HVNF6E89HDT1ABRFWJ8C8`

String (required) - The name of the channel to query message history.

#### `expandReactions` {/* #connection/listMessages.expandReactions */}

Boolean (optional) - If `true`, the `reactions` object on each returned message is enriched with an `items` array per reaction containing the `uid` and `umd` of each reacting user. Default is `false`, which returns aggregated counts only. Messages with no reactions omit the field when `false` and emit `{}` when `true`.

#### `reverse` {/* #connection/listMessages.reverse */}

Boolean (optional) - If set to `true`, lists messages from newest to oldest. Default is `false`.
Expand Down Expand Up @@ -122,7 +128,9 @@ fetch(
"meta": {
"uid": "Jim",
"umd": null
}
},
// highlight-next-line
"reactions": { "thumbsup": { "count": 2 }, "heart": { "count": 1 } }
},
{
"id": "01JA3S0GB6S3WNTV2S1RJ421TH",
Expand Down Expand Up @@ -152,7 +160,93 @@ fetch(
"meta": {
"uid": "Pam",
"umd": null
},
// highlight-next-line
"reactions": { "heart": { "count": 3 } }
}
]
}
```

With `expandReactions: true`, each entry also has an `items` array with one element per reacting user:

```json
{
"messages": [
{
"id": "01JA3S0P7FB7WVYS67X316M32S",
"event": "my-event",
"channel": "my-channel",
"data": "Wow. Can we make it a different moment?",
"meta": { "uid": "Jim", "umd": null },
// highlight-start
"reactions": {
"thumbsup": {
"count": 2,
"items": [
{ "uid": "Jim", "umd": { "name": "Jim Halpert" } },
{ "uid": "Pam" }
]
},
"heart": {
"count": 1,
"items": [{ "uid": "Dwight" }]
}
}
// highlight-end
}
]
}
```

## `connection/listReactions` {/* #connection/listReactions */}

List the individual reactions on a specific stored message, including who reacted. The connection must be subscribed to the channel.

#### `channel` {/* #connection/listReactions.channel */}

`String` (required) - The name of the channel where the message was published.

#### `messageId` {/* #connection/listReactions.messageId */}

`String` (required) - The ID of the message to list reactions for.

### Example {/* #connection/listReactions--example */}

#### Request {/* #connection/listReactions--example-request */}

```javascript
fetch(
"https://r6zcm2.lambda-url.us-east-1.on.aws/connection/listReactions?connectionId=fjlb_eHLIAMCKRg%3d&connectionSecret=SZy32Etv0KIbe4Jod6KH",
{
method: "POST",
body: JSON.stringify({
channel: "my-channel",
messageId: "01JA3S0P7FB7WVYS67X316M32S",
}),
},
)
```

#### Response {/* #connection/listReactions--example-response */}

Expect a `200 OK` status code for successful reads.

```json
{
"reactions": [
{
"reaction": "thumbsup",
"uid": "Jim",
"umd": { "name": "Jim Halpert" }
},
{
"reaction": "thumbsup",
"uid": "Pam"
},
{
"reaction": "heart",
"uid": "Dwight"
}
]
}
Expand Down
Loading