diff --git a/src/data/nav/chat.ts b/src/data/nav/chat.ts
index ad8ca06013..077f5ecd7a 100644
--- a/src/data/nav/chat.ts
+++ b/src/data/nav/chat.ts
@@ -58,6 +58,10 @@ export default {
name: 'SDK setup',
link: '/docs/chat/setup',
},
+ {
+ name: 'Authentication',
+ link: '/docs/chat/authentication',
+ },
{
name: 'Connections',
link: '/docs/chat/connect',
diff --git a/src/data/nav/spaces.ts b/src/data/nav/spaces.ts
index bc53734db3..e230c99349 100644
--- a/src/data/nav/spaces.ts
+++ b/src/data/nav/spaces.ts
@@ -21,6 +21,10 @@ export default {
name: 'SDK setup',
link: '/docs/spaces/setup',
},
+ {
+ name: 'Authentication',
+ link: '/docs/spaces/authentication',
+ },
{
name: 'React Hooks',
link: '/docs/spaces/react',
diff --git a/src/pages/docs/auth/capabilities.mdx b/src/pages/docs/auth/capabilities.mdx
index 34a9d2f663..90b7ba396c 100644
--- a/src/pages/docs/auth/capabilities.mdx
+++ b/src/pages/docs/auth/capabilities.mdx
@@ -103,68 +103,87 @@ Ably Tokens and JWTs are issued from an existing API key and their capabilities
If an API key must be shared with a third party, then it is recommended that [the principle of least privilege](https://en.wikipedia.org/wiki/Principle_of_least_privilege) is considered, assigning only the capabilities needed by that third party. Thus, any Ably requests authenticated using that API key or Ably-compatible tokens associated with that API key, will be restricted to the capabilities assigned to the API key.
-Capabilities can be set when creating a token or token request, as shown in the following example:
+#### Using JWT (recommended)
+
+Set capabilities in a JWT using the `x-ably-capability` claim. No Ably SDK is required—any JWT library will work:
```javascript
- var tokenParams = { clientId: 'foo', capability: JSON.stringify(capability) };
- const tokenRequest = await ably.auth.createTokenRequest(tokenParams);
-```
+// Server-side: Create JWT with capabilities using any JWT library
+import jwt from 'jsonwebtoken';
-```python
-token_params = {
- 'client_id': 'foo',
- 'capability': json.dumps(capability)
-}
+const [keyName, keySecret] = process.env.ABLY_API_KEY.split(':');
-token_request = await ably_rest.auth.create_token_request(token_params)
+const ablyJwt = jwt.sign(
+ {
+ 'x-ably-capability': JSON.stringify({
+ 'your-namespace:*': ['publish', 'subscribe', 'presence'],
+ 'notifications': ['subscribe'],
+ }),
+ 'x-ably-clientId': userId,
+ },
+ keySecret,
+ { algorithm: 'HS256', keyid: keyName, expiresIn: '1h' }
+);
```
-```php
-$tokenParams = [
- 'clientId' => 'client@example.com',
- 'capability' => json_encode($capability)
-];
-$tokenRequest = $rest->auth->createTokenRequest($tokenParams);
+```python
+# Server-side: Create JWT with capabilities using any JWT library
+import jwt
+import json
+import time
+import os
+
+key_name, key_secret = os.environ['ABLY_API_KEY'].split(':')
+
+now = int(time.time())
+ably_jwt = jwt.encode(
+ {
+ 'iat': now,
+ 'exp': now + 3600,
+ 'x-ably-capability': json.dumps({
+ 'your-namespace:*': ['publish', 'subscribe', 'presence'],
+ 'notifications': ['subscribe'],
+ }),
+ 'x-ably-clientId': user_id,
+ },
+ key_secret,
+ algorithm='HS256',
+ headers={'kid': key_name}
+)
```
+
-```go
-rest, _ := ably.NewREST(
- ably.WithKey("xVLyHw.I2jW-g:dOTlhEt-nIubVAPMAUJnGv-_F8BZ7xNYnXdajpGaISg"))
+#### Using TokenRequest (alternative)
-// Define the capability
-capability := map[string][]string{
- "*": {"*"},
-}
+If JWT is not suitable, use the Ably SDK to create a TokenRequest with capabilities:
-capabilityJSON, err := json.Marshal(capability)
-if err != nil {
- log.Fatalf("Failed to marshal capability: %v", err)
-}
-
-// Define the token parameters
-tokenParams := &ably.TokenParams{
- ClientID: "foo",
- Capability: string(capabilityJSON),
-}
-
-// Create a token request
-tokenRequest, err := rest.Auth.CreateTokenRequest(tokenParams)
-if err != nil {
- log.Fatalf("Failed to create token request: %v", err)
-}
+
+```javascript
+// Server-side with Ably SDK
+const ably = new Ably.Rest(process.env.ABLY_API_KEY);
+const tokenRequest = await ably.auth.createTokenRequest({
+ clientId: 'user-123',
+ capability: JSON.stringify({
+ 'your-namespace:*': ['publish', 'subscribe'],
+ }),
+});
```
-```flutter
-final tokenParams = ably.TokenParams(
- clientId: 'foo',
- capability: jsonEncode(capability),
-);
-
-final tokenRequest = await rest.auth.createTokenRequest(tokenParams: tokenParams);
+```python
+# Server-side with Ably SDK
+ably = AblyRest(os.environ['ABLY_API_KEY'])
+token_request = await ably.auth.create_token_request({
+ 'client_id': 'user-123',
+ 'capability': json.dumps({
+ 'your-namespace:*': ['publish', 'subscribe'],
+ }),
+})
```
+See [token authentication](/docs/auth/token#token-request) for complete TokenRequest examples in all languages.
+
### Token capability determination
The capabilities for tokens are determined based on those of the issuing API key and those requested by the token.
@@ -179,8 +198,8 @@ Using the following example, an API key exists with the listed capabilities. If
```javascript
// API key capabilities:
{
- 'chat': ['publish', 'subscribe', 'presence'],
- 'status': ['subscribe']
+ 'your-namespace': ['publish', 'subscribe', 'presence'],
+ 'notifications': ['subscribe']
}
// Token request that doesn't specify any capabilities:
@@ -188,19 +207,19 @@ Using the following example, an API key exists with the listed capabilities. If
// Resulting token capabilities:
{
- 'chat': ['publish', 'subscribe', 'presence'],
- 'status': ['subscribe']
+ 'your-namespace': ['publish', 'subscribe', 'presence'],
+ 'notifications': ['subscribe']
}
```
```python
# API key capabilities:
# {
-# "chat": ["publish", "subscribe", "presence"],
-# "status": ["subscribe"]
+# "your-namespace": ["publish", "subscribe", "presence"],
+# "notifications": ["subscribe"]
# }
-// Token request that doesn't specify any capabilities:
+# Token request that doesn't specify any capabilities:
token = await ably.auth.create_token_request(
{
"clientId": "client@example.com",
@@ -209,37 +228,16 @@ token = await ably.auth.create_token_request(
# Resulting token capabilities:
# {
-# "chat": ["publish", "subscribe", "presence"],
-# "status": ["subscribe"]
+# "your-namespace": ["publish", "subscribe", "presence"],
+# "notifications": ["subscribe"]
# }
```
-```php
-// API key capabilities:
-//{
-// 'chat': ['publish', 'subscribe', 'presence'],
-// 'status': ['subscribe']
-//}
-
-// Token request that doesn't specify any capabilities:
-$tokenParams = [
- 'clientId' => 'client@example.com',
- 'ttl' => 3600 * 1000, // ms
-];
-$tokenRequest = $rest->auth->requestToken($tokenParams);
-
-// Resulting token capabilities:
-//{
-// 'chat': ['publish', 'subscribe', 'presence'],
-// 'status': ['subscribe']
-//}
-```
-
```go
// API key capabilities:
// {
-// "chat": ["publish", "subscribe", "presence"],
-// "status": ["subscribe"]
+// "your-namespace": ["publish", "subscribe", "presence"],
+// "notifications": ["subscribe"]
// }
rest, _ := ably.NewREST(
@@ -259,24 +257,8 @@ if err != nil {
// Resulting token capabilities:
// {
-// "chat": ["publish", "subscribe", "presence"],
-// "status": ["subscribe"]
-// }
-```
-
-```flutter
-// API key capabilities:
-// {
-// 'chat': ['publish', 'subscribe', 'presence'],
-// 'status': ['subscribe']
-// }
-
-final tokenRequest = await realtime.auth.requestToken(tokenParams: tokenParams);
-
-// Resulting token capabilities:
-// {
-// 'chat': ['publish', 'subscribe', 'presence'],
-// 'status': ['subscribe']
+// "your-namespace": ["publish", "subscribe", "presence"],
+// "notifications": ["subscribe"]
// }
```
@@ -291,38 +273,38 @@ Using the following example, an API key exists with the listed capabilities. If
```javascript
// API key capabilities:
{
- 'chat:*': ['publish', 'subscribe', 'presence'],
- 'status': ['subscribe', 'history'],
+ 'your-namespace:*': ['publish', 'subscribe', 'presence'],
+ 'notifications': ['subscribe', 'history'],
'alerts': ['subscribe']
}
// Token request that specifies capabilities:
const tokenDetails = await auth.requestToken({ capability: {
- 'chat:bob': ['subscribe'], // only 'subscribe' intersects
- 'status': ['*'], // '*'' intersects with 'subscribe'
- 'secret': ['publish', 'subscribe'] // key does not have access to 'secret' channel
+ 'your-namespace:user-123': ['subscribe'], // only 'subscribe' intersects
+ 'notifications': ['*'], // '*'' intersects with 'subscribe'
+ 'private': ['publish', 'subscribe'] // key does not have access to 'private' channel
}});
// Resulting token capabilities:
{
- 'chat:bob': ['subscribe'],
- 'status': ['subscribe', 'history']
+ 'your-namespace:user-123': ['subscribe'],
+ 'notifications': ['subscribe', 'history']
}
```
```python
# API key capabilities:
# {
-# "chat:*": ["publish", "subscribe", "presence"],
-# "status": ["subscribe", "history"],
+# "your-namespace:*": ["publish", "subscribe", "presence"],
+# "notifications": ["subscribe", "history"],
# "alerts": ["subscribe"]
# }
# Token request that specifies capabilities:
capabilities = {
- "chat:bob": ["subscribe"], # only "subscribe" intersects
- "status": ["*"], # "*" intersects with "subscribe"
- "secret": ["publish", "subscribe"] # key does not have access to "secret" channel
+ "your-namespace:user-123": ["subscribe"], # only "subscribe" intersects
+ "notifications": ["*"], # "*" intersects with "subscribe"
+ "private": ["publish", "subscribe"] # key does not have access to "private" channel
}
token_details = await ably_rest.auth.request_token({
@@ -331,48 +313,16 @@ token_details = await ably_rest.auth.request_token({
# Resulting token capabilities:
# {
-# "chat:bob": ["subscribe"],
-# "status": ["subscribe", "history"]
+# "your-namespace:user-123": ["subscribe"],
+# "notifications": ["subscribe", "history"]
# }
```
-```php
-/**
- * API key capabilities:
- * {
- * 'chat:*': ['publish', 'subscribe', 'presence'],
- * 'status': ['subscribe', 'history'],
- * 'alerts': ['subscribe']
- * }
- */
-
-// Token request that specifies capabilities:
-$capabilities = [
- 'chat:bob' => ['subscribe'], // only 'subscribe' intersects
- 'status' => ['*'], // '*' intersects with 'subscribe'
- 'secret' => ['publish', 'subscribe'] // key does not have access to 'secret' channel
-];
-
-$tokenDetails = $rest
- ->auth
- ->requestToken(
- ['capability' => json_encode($capabilities)]
- );
-
-/**
- * Resulting token capabilities:
- * {
- * 'chat:bob': ['subscribe'],
- * 'status': ['subscribe', 'history']
- * }
- */
-```
-
```go
// API key capabilities:
// {
-// "chat:*": ["publish", "subscribe", "presence"],
-// "status": ["subscribe", "history"],
+// "your-namespace:*": ["publish", "subscribe", "presence"],
+// "notifications": ["subscribe", "history"],
// "alerts": ["subscribe"]
// }
@@ -382,9 +332,9 @@ rest, _ := ably.NewREST(
// Define the capabilities
capabilities := map[string][]string{
- "chat:bob": {"subscribe"},
- "status": {"*"},
- "secret": {"publish", "subscribe"},
+ "your-namespace:user-123": {"subscribe"},
+ "notifications": {"*"},
+ "private": {"publish", "subscribe"},
}
capabilitiesJSON, err := json.Marshal(capabilities)
@@ -405,34 +355,8 @@ if err != nil {
// Resulting token capabilities:
// {
-// "chat:bob": ["subscribe"],
-// "status": ["subscribe", "history"]
-// }
-```
-
-```flutter
-// API key capabilities:
-// {
-// 'chat:bob': ['subscribe'],
-// 'status': ['*'],
-// 'secret': ['publish', 'subscribe']
-// }
-
-final tokenParams = ably.TokenParams(
- capability: jsonEncode({
- 'chat:bob': ['subscribe'],
- 'status': ['*'],
- 'secret': ['publish', 'subscribe']
- }),
-);
-
-final tokenDetails = await rest.auth.requestToken(tokenParams: tokenParams);
-
-// Resulting token capabilities:
-// {
-// 'chat:bob': ['subscribe'],
-// 'secret': ['publish','subscribe']
-// 'status': ['subscribe', 'history']
+// "your-namespace:user-123": ["subscribe"],
+// "notifications": ["subscribe", "history"]
// }
```
@@ -447,50 +371,32 @@ Using the following example, an API key exists with the listed capabilities. If
```javascript
// API key capabilities:
{
- 'chat': ['*']
+ 'your-namespace': ['*']
}
// Token request that specifies capabilities:
const tokenDetails = await auth.requestToken({ capability: {
- 'status': ['*']
+ 'other-namespace': ['*']
}});
```
```python
# API key capabilities:
# {
-# "chat": ["*"]
+# "your-namespace": ["*"]
# }
token_details = await ably_rest.auth.request_token({
'capability': json.dumps({
- {
- "status": ["*"]
- }
+ "other-namespace": ["*"]
})
})
```
-```php
-/**
- * API key capabilities:
- * {
- * 'chat': ['*']
- * }
- */
-
-// Token request that specifies capabilities:
-$tokenDetails = $rest
- ->auth
- ->requestToken(
- ['capability' => json_encode(['status' => ['*']])]
- );
-```
-
```go
// API key capabilities:
// {
-// "chat": ["*"]
+// "your-namespace": ["*"]
// }
rest, _ := ably.NewREST(
@@ -498,7 +404,7 @@ rest, _ := ably.NewREST(
// Define the capabilities
capabilities := map[string][]string{
- "status": {"*"},
+ "other-namespace": {"*"},
}
capabilitiesJSON, err := json.Marshal(capabilities)
@@ -517,20 +423,6 @@ if err != nil {
log.Fatalf("Failed to request token: %v", err)
}
```
-
-```flutter
-// API key capabilities:
-// {
-// 'status': ['*']
-// }
-final tokenParams = ably.TokenParams(
- capability: jsonEncode({
- 'status': ['*']
- }),
-);
-
-final tokenDetails = await realtime.auth.requestToken(tokenParams: tokenParams);
-```
#### Ably JWT capability determination
@@ -542,6 +434,10 @@ Capabilities are determined for [Ably JWTs](/docs/auth/token#jwt) in the followi
## Custom restrictions on channels
+
+
It is possible for JWTs to contain authenticated claims for users that can be used to allow or disallow certain interactions in your channels.
Messages can be annotated with trusted metadata copied from the client's authentication token by Ably servers. Clients are unable to directly publish messages with user claim metadata, and claims contained within the authentication token are signed to prevent tampering. Claims can be scoped to individual channels or to namespaces of [channels](/docs/channels). The most specific user claim will be added to the message as part of the `extras` object. Note that this does not apply to presence or metadata messages.
@@ -555,8 +451,8 @@ To set the trusted fields you need to include `ably.channel.*` in your JWT authe
'name': 'John Doe',
'x-ably-capability': <...>,
'x-ably-clientId': <...>,
- 'ably.channel.chat1': 'admin', // the user is an admin for the chat1 channel
- 'ably.channel.chat:*': 'moderator', // the user is a moderator in channels within the chat namespace
+ 'ably.channel.your-channel': 'admin', // the user is an admin for a specific channel
+ 'ably.channel.your-namespace:*': 'moderator', // the user is a moderator in all channels within a namespace
'ably.channel.*': 'guest', // the user is a guest in all other channels
}
```
@@ -567,47 +463,23 @@ claims = {
"name": "John Doe",
"x-ably-capability": "<...>",
"x-ably-clientId": "<...>",
- "ably.channel.chat1": "admin", # the user is an admin for the chat1 channel
- "ably.channel.chat:*": "moderator", # the user is a moderator in channels within the chat namespace
+ "ably.channel.your-channel": "admin", # the user is an admin for a specific channel
+ "ably.channel.your-namespace:*": "moderator", # the user is a moderator in all channels within a namespace
"ably.channel.*": "guest" # the user is a guest in all other channels
}
```
-```php
-$claims = [
- 'sub' => '1234567890',
- 'name' => 'John Doe',
- 'x-ably-capability' => '<...>',
- 'x-ably-clientId' => '<...>',
- 'ably.channel.chat1' => 'admin', // the user is an admin for the chat1 channel
- 'ably.channel.chat =>*' => 'moderator', // the user is a moderator in channels within the chat namespace
- 'ably.channel.*' => 'guest' // the user is a guest in all other channels
-];
-```
-
```go
claims := map[string]interface{}{
- "sub": "1234567890",
- "name": "John Doe",
- "x-ably-capability": "<...>",
- "x-ably-clientId": "<...>",
- "ably.channel.chat1": "admin",
- "ably.channel.chat:*": "moderator",
- "ably.channel.*": "guest",
+ "sub": "1234567890",
+ "name": "John Doe",
+ "x-ably-capability": "<...>",
+ "x-ably-clientId": "<...>",
+ "ably.channel.your-channel": "admin",
+ "ably.channel.your-namespace:*": "moderator",
+ "ably.channel.*": "guest",
}
```
-
-```flutter
-final claims = {
- 'sub': '1234567890',
- 'name': 'John Doe',
- 'x-ably-capability': '<...>',
- 'x-ably-clientId': '<...>',
- 'ably.channel.chat1': 'admin',
- 'ably.channel.chat:*': 'moderator',
- 'ably.channel.*': 'guest',
-};
-```
The claims from the token are copied into messages, allowing them to be checked for permission:
@@ -638,17 +510,14 @@ func fromModerator(message map[string]interface{}) bool {
return false
}
```
-
-```flutter
-bool fromModerator(Message message) {
- final userClaim = message.extras['userClaim'];
- return userClaim != null && userClaim == 'moderator';
-}
-```
## Using JWT for per connection publish rate limits
+
+
JWTs may specify publish rate limits for a user on particular channels. These limits can be used to prevent any individual user from sending an excessive number of messages in a short period of time.
An example use case is in a large live chat where you may wish to limit users to posting messages no more than once every 10 seconds.
@@ -666,8 +535,8 @@ The following is an example of setting different rate limits for different chann
'name': 'John Doe',
'x-ably-capability': <...>,
'x-ably-clientId': <...>,
- 'ably.limits.publish.perAttachment.maxRate.chat1': 10, // the user can publish 10 messages per second in channel chat1
- 'ably.limits.publish.perAttachment.maxRate.chat:*': 0.1 // the user can publish a message every 10 seconds in all channels within the chat namespace
+ 'ably.limits.publish.perAttachment.maxRate.your-channel': 10, // the user can publish 10 messages per second in a specific channel
+ 'ably.limits.publish.perAttachment.maxRate.your-namespace:*': 0.1 // the user can publish a message every 10 seconds in all channels within a namespace
}
```
@@ -677,41 +546,19 @@ claims = {
"name": "John Doe",
"x-ably-capability": "<...>",
"x-ably-clientId": "<...>",
- "ably.limits.publish.perAttachment.maxRate.chat1": 10, # the user can publish 10 messages per second in channel chat1
- "ably.limits.publish.perAttachment.maxRate.chat:*": 0.1 # the user can publish a message every 10 seconds in all channels within the chat namespace
+ "ably.limits.publish.perAttachment.maxRate.your-channel": 10, # the user can publish 10 messages per second in a specific channel
+ "ably.limits.publish.perAttachment.maxRate.your-namespace:*": 0.1 # the user can publish a message every 10 seconds in all channels within a namespace
}
```
-```php
-$claims = [
- 'sub' => '1234567890',
- 'name' => 'John Doe',
- 'x-ably-capability' => '<...>',
- 'x-ably-clientId' => '<...>',
- 'ably.limits.publish.perAttachment.maxRate.chat1' => 10, // the user can publish 10 messages per second in channel chat1
- 'ably.limits.publish.perAttachment.maxRate.chat:*' => 0.1 // the user can publish a message every 10 seconds in all channels within the chat namespace
-]
-```
-
```go
claims := map[string]interface{}{
"sub": "1234567890",
"name": "John Doe",
"x-ably-capability": "<...>",
"x-ably-clientId": "<...>",
- "ably.limits.publish.perAttachment.maxRate.chat1": 10.0, // the user can publish 10 messages per second in channel chat1
- "ably.limits.publish.perAttachment.maxRate.chat:*": 0.1, // the user can publish a message every 10 seconds in all channels within the chat namespace
+ "ably.limits.publish.perAttachment.maxRate.your-channel": 10.0, // the user can publish 10 messages per second in a specific channel
+ "ably.limits.publish.perAttachment.maxRate.your-namespace:*": 0.1, // the user can publish a message every 10 seconds in all channels within a namespace
}
```
-
-```flutter
-final claims = {
- 'sub': '1234567890',
- 'name': 'John Doe',
- 'x-ably-capability': '<...>', // Replace with actual capability
- 'x-ably-clientId': '<...>', // Replace with actual client ID
- 'ably.limits.publish.perAttachment.maxRate.chat1': 10, // the user can publish 10 messages per second in channel chat1
- 'ably.limits.publish.perAttachment.maxRate.chat:*': 0.1 // the user can publish a message every 10 seconds in all channels within the chat namespace
-};
-```
diff --git a/src/pages/docs/auth/identified-clients.mdx b/src/pages/docs/auth/identified-clients.mdx
index e4754fa566..df569cd835 100644
--- a/src/pages/docs/auth/identified-clients.mdx
+++ b/src/pages/docs/auth/identified-clients.mdx
@@ -48,167 +48,47 @@ You can use [token authentication](/docs/auth/token) to set an explicit `clientI
For example, when publishing a message, the `clientId` attribute of the message will be pre-populated with that `clientId`. Entering presence will also implicitly use that `clientId`.
-The following example demonstrates how to issue an [Ably TokenRequest](/docs/auth/token#token-request) with an explicit `clientId`:
+#### Using JWT (recommended)
-
-```realtime_javascript
- const realtime = new Ably.Realtime({ key: '{{API_KEY}}' });
- const tokenRequest = await realtime.auth.createTokenRequest({ clientId: 'Bob'});
-```
-
-```realtime_nodejs
- const realtime = new Ably.Realtime({ key: '{{API_KEY}}' });
- const tokenRequest = await realtime.auth.createTokenRequest({ clientId: 'Bob'});
-```
-
-```realtime_ruby
- realtime = Ably::Realtime.new(key: '{{API_KEY}}')
- realtime.auth.createTokenRequest(client_id: 'Bob') do |token_request|
- # ... issue the TokenRequest to a client ...
- end
-```
-
-```realtime_python
- realtime = AblyRealtime(key='{{API_KEY}}')
- token_request = await realtime.auth.create_token_request({'client_id': 'Bob'})
- # ... issue the TokenRequest to a client ...
-```
-
-```realtime_java
- ClientOptions options = new ClientOptions();
- options.key = "{{API_KEY}}";
- AblyRealtime realtime = new AblyRealtime(options);
- TokenParams tokenParams = new TokenParams();
- tokenParams.clientId = "Bob";
- TokenRequest tokenRequest;
- tokenRequest = realtime.auth.createTokenRequest(tokenParams, null);
- /* ... issue the TokenRequest to a client ... */
-```
+The recommended approach is to create a JWT with the `x-ably-clientId` claim. This requires no Ably SDK on your server—any JWT library will work:
-```realtime_csharp
- AblyRealtime realtime = new AblyRealtime("{{API_KEY}}");
- TokenParams tokenParams = new TokenParams {ClientId = "Bob"};
- string tokenRequest = await realtime.Auth.CreateTokenRequestAsync(tokenParams);
- /* ... issue the TokenRequest to a client ... */
-```
-
-```realtime_objc
- ARTRealtime *realtime = [[ARTRealtime alloc] initWithKey:@"{{API_KEY}}"];
- ARTTokenParams *tokenParams = [[ARTTokenParams alloc] initWithClientId:@"Bob"];
- [realtime.auth createTokenRequest:tokenParams options:nil
- callback:^(ARTTokenRequest *tokenRequest NSError *error) {
- // ... issue the TokenRequest to a client ...
- }];
-```
-
-```realtime_swift
- let realtime = ARTRealtime(key: "{{API_KEY}}")
- let tokenParams = ARTTokenParams(clientId: "Bob")
- realtime.auth.createTokenRequest(tokenParams, options: nil) { tokenRequest, error in
- // ... issue the TokenRequest to a client ...
- }
-```
-
-```realtime_go
-realtime, _ := ably.NewRealtime(
- ably.WithKey("{{API_KEY}}"))
-params := &ably.TokenParams{
- ClientID: "Bob",
-}
-tokenRequest, _ := realtime.Auth.CreateTokenRequest(params)
-```
-
-```realtime_flutter
-final realtime = ably.Realtime(options: ably.ClientOptions(key: '{{API_KEY}}'));
-final tokenRequest = await realtime.auth.createTokenRequest(
- tokenParams: ably.TokenParams(clientId: 'Bob'),
+
+```javascript
+const ablyJwt = jwt.sign(
+ {
+ 'x-ably-capability': JSON.stringify({ '*': ['*'] }),
+ 'x-ably-clientId': userId, // Set the trusted clientId
+ },
+ keySecret,
+ { algorithm: 'HS256', keyid: keyName, expiresIn: '1h' }
);
```
+
-```rest_javascript
- const rest = new Ably.Rest({ key: '{{API_KEY}}' });
- const tokenRequest = await realtime.auth.createTokenRequest({ clientId: 'Bob'});
-```
-
-```rest_nodejs
- const rest = new Ably.Rest({ key: '{{API_KEY}}' });
- const tokenRequest = await realtime.auth.createTokenRequest({ clientId: 'Bob'});
-```
-
-```rest_ruby
- rest = Ably::Rest.new(key: '{{API_KEY}}')
- token_request = rest.auth.create_token_request(client_id: 'Bob')
- # ... issue the TokenRequest to a client ...
-```
-
-```rest_python
- rest = AblyRest(key='{{API_KEY}}')
- token_request = await rest.auth.create_token_request({'client_id': 'Bob'})
- # ... issue the TokenRequest to a client ...
-```
-
-```rest_php
- $rest = new Ably\AblyRest(
- ['key' => '{{API_KEY}}']
- );
- $tokenRequest = $rest->auth->createTokenRequest(
- ['clientId' => 'Bob']
- );
- // ... issue the TokenRequest to a client ...
-```
-
-```rest_java
- ClientOptions options = new ClientOptions();
- options.key = "{{API_KEY}}";
- AblyRest rest = new AblyRest(options);
- TokenParams tokenParams = new TokenParams();
- tokenParams.clientId = "Bob";
- TokenRequest tokenRequest;
- tokenRequest = rest.auth.createTokenRequest(tokenParams, null);
- /* ... issue the TokenRequest to a client ... */
-```
+See [token authentication scenarios](/docs/auth/token#scenario-standard) for complete server and client examples.
-```rest_csharp
- AblyRest rest = new AblyRest(new ClientOptions {Key = "{{API_KEY}}"});
- TokenParams tokenParams = new TokenParams {ClientId = "Bob"};
- string tokenRequest = await rest.Auth.CreateTokenRequestAsync(tokenParams);
- // ... issue the TokenRequest to a client ...
-```
+#### Using TokenRequest (alternative)
-```rest_objc
- ARTRest *rest = [[ARTRest alloc] initWithKey:@"{{API_KEY}}"];
- ARTTokenParams *tokenParams = [[ARTTokenParams alloc] initWithClientId:@"Bob"];
- [rest.auth createTokenRequest:tokenParams options:nil
- callback:^(ARTTokenRequest *tokenRequest, NSError *error) {
- // ... issue the TokenRequest to a client ...
- }];
-```
+If JWT is not suitable for your use case, you can use the Ably SDK to create a TokenRequest with an explicit `clientId`:
-```rest_swift
- let rest = ARTRest(key: "{{API_KEY}}")
- let tokenParams = ARTTokenParams(clientId: "Bob")
- rest.auth.createTokenRequest(tokenParams, options: nil) { tokenRequest, error in
- // ... issue the TokenRequest to a client ...
- }
-```
-
-```rest_go
-rest, _ := ably.NewREST(
- ably.WithKey("{{API_KEY}}"))
-params := &ably.TokenParams{
- ClientID: "Bob",
-}
-tokenRequest, _ := rest.Auth.CreateTokenRequest(params)
+
+```javascript
+// Server-side with Ably SDK
+const ably = new Ably.Rest(process.env.ABLY_API_KEY);
+const tokenRequest = await ably.auth.createTokenRequest({ clientId: 'user-123' });
+// Return tokenRequest to client
```
-```rest_flutter
-final rest = ably.Rest(options: ably.ClientOptions(key: '{{API_KEY}}'));
-final tokenRequest = await rest.auth.createTokenRequest(
- tokenParams: ably.TokenParams(clientId: 'Bob'),
-);
+```python
+# Server-side with Ably SDK
+ably = AblyRest(os.environ['ABLY_API_KEY'])
+token_request = await ably.auth.create_token_request({'client_id': 'user-123'})
+# Return token_request to client
```
+See [token authentication](/docs/auth/token#token-request) for complete TokenRequest examples in all languages.
+
### Wildcard token auth
You can use [token authentication](/docs/auth/token) to set a wildcard `clientId` using a value of `*` when creating a token. Clients are then able to assume any identity in their operations, such as when publishing a message or entering presence.
diff --git a/src/pages/docs/auth/index.mdx b/src/pages/docs/auth/index.mdx
index 51949e95e5..5d45e3c797 100644
--- a/src/pages/docs/auth/index.mdx
+++ b/src/pages/docs/auth/index.mdx
@@ -20,91 +20,88 @@ redirect_from:
- /docs/ids-and-keys
---
-Before a client or server can issue requests to Ably, such as subscribe to channels, or publish messages, it must authenticate with Ably. Authentication requires an Ably API key.
+Before a client or server can issue requests to Ably, such as subscribe to channels, or publish messages, it must authenticate with Ably.
-## Authentication terminology
+## Recommended authentication
-The following terminology helps explain authentication, authorization, and identification in the context of the Ably service:
+| Environment | Recommended Method | Details |
+|-------------|-------------------|---------|
+| **Client-side** (browsers, mobile apps) | [Token authentication](/docs/auth/token) | Your server creates tokens; clients use `authCallback` to fetch them |
+| **Server-side** (Node.js, Python, etc.) | [Basic authentication](/docs/auth/basic) | Use your API key directly in trusted environments |
-"Authentication" is the process of deciding, based on the presented credentials, whether or not an entity may interact with the Ably service. The credentials may be presented explicitly using [Basic Authentication](/docs/auth/basic) or [Token Authentication](/docs/auth/token), or in some cases the entity authenticating may prove possession of the credentials with a signed Token Request that is subsequently used to generate a valid token to be used for Token Authentication. When authenticating with Ably, the credentials are either an API key or an auth token.
+
-"Authenticated client" is a client of the Ably service that has been successfully authenticated.
+### Quick start
-"Authorization" is the process of deciding whether or not a given entity (usually authenticated) is allowed to perform a given operation. In Ably, authorization for most operations is based on the [capabilities](/docs/auth/capabilities) associated with the key or token that was used to authenticate a client.
+
+```javascript
+// Client-side: fetch token from your server
+const realtime = new Ably.Realtime({
+ authCallback: async (tokenParams, callback) => {
+ const response = await fetch('/api/ably-token');
+ callback(null, await response.text());
+ },
+});
+```
+
-"Identified client" is an authenticated client with a specific claimed client identity, or `clientId`, whose credentials are verified as confirming that identity. See the [identified clients](/docs/auth/identified-clients) documentation for more information.
+See [token authentication](/docs/auth/token#scenario-standard) for complete server and client examples.
-
+## Authentication terminology
+
+- **Authentication**: The process of verifying credentials (API key or token) to allow interaction with Ably.
+- **Authorization**: Determining what operations an authenticated client can perform, based on [capabilities](/docs/auth/capabilities).
+- **Identified client**: A client with a verified identity (`clientId`). See [identified clients](/docs/auth/identified-clients).
## Ably API keys
-Every Ably app can have one or more API keys associated with it in order to authenticate directly with Ably, or to issue tokens with. API keys can be created with different [capabilities](/docs/auth/capabilities) and any tokens issued using that API key can only permit a subset of those capabilities.
+Every Ably app can have one or more API keys associated with it. API keys authenticate directly with Ably or issue tokens. Keys can have different [capabilities](/docs/auth/capabilities), and tokens issued from a key can only have a subset of those capabilities.
### API key format
-An Ably API key string has the following format: `I2E_JQ.OqUdfg:EVKVTCBlzLBPYJiCZTsIW_pqylJ9WVRB5K9P19Ap1y0`.
+An Ably API key string has the following format: `I2E_JQ.OqUdfg:EVKVTCBlzLBPYJiCZTsIW_pqylJ9WVRB5K9P19Ap1y0`
-The API key is made up of three parts:
+The API key has three parts:
-1. `I2E_JQ` is the public app ID (the part before the first period)
-2. `OqUdfg` is the public app key ID (the part after the period and before the colon). `I2E_JQ.OqUdfg` is the public API key ID (both the public app ID and app key ID together)
-3. `EVKVTCBlzLBPYJiCZTsIW_pqylJ9WVRB5K9P19Ap1y0` is the API key secret and should never be shared with untrusted parties (the part after the colon)
-
-The API key secret is private and should never be made public. This API key string is used in all Ably SDKs and for authentication with the REST API.
+1. `I2E_JQ` - the public app ID
+2. `OqUdfg` - the public key ID (`I2E_JQ.OqUdfg` together form the public API key ID)
+3. `EVKVTCBlzLBPYJiCZTsIW_pqylJ9WVRB5K9P19Ap1y0` - the API key secret (never share this)
### Create an API key
-API keys are created in the [Ably dashboard](https://ably.com/dashboard). You can also create an API key programmatically using the [Control API](/docs/platform/account/control-api).
-
-To create an API key in the Ably dashboard:
+API keys are created in the [Ably dashboard](https://ably.com/dashboard) or programmatically via the [Control API](/docs/platform/account/control-api).
-1. In your [Ably dashboard](https://ably.com/dashboard) click the API Keys tab.
-2. Click the **Create a new API key** button.
-3. Enter a name for your API key - this will help you identify the specific key when you have many keys created.
-4. Select the [capabilities](/docs/auth/capabilities) you want to apply to the key. Clients connecting with this key will be restricted to these capabilities.
-5. Optionally you can select whether to make tokens generated from this key to be revocable or not.
-6. Optionally select whether you want to restrict the scope of the key to channels, queues, or specific channels and queues using resource names and wildcards.
+To create an API key in the dashboard:
-
-## Ably Tokens
-
-Ably Tokens can be used to authenticate with Ably in the following ways:
-
-* Ably [TokenRequest](#token-request) is created by your servers and passed to clients.
-* An [Ably Token](#ably-token) is issued by your servers and passed to clients.
-
-Note that the machine on which you are running your auth server should have an accurate clock, as tokens and `TokenRequest` contain a timestamp. You can use an [NTP daemon](https://en.wikipedia.org/wiki/Ntpd), or if you are not able to control your server's clock, you can wish to use the `queryTime` [auth option](/docs/api/rest-sdk/types#auth-options).
-
-### Ably TokenRequest
-
-Using an Ably SDK, a `TokenRequest` is [generated from your server](/docs/api/realtime-sdk/authentication#create-token-request) and returned to the client-side SDK instance. The client-side SDK instance then uses the [`TokenRequest`](/docs/api/realtime-sdk/types#token-request) to request an [Ably Token](/docs/api/realtime-sdk/authentication#request-token) from Ably, and subsequently authenticates using that [Ably Token](/docs/api/realtime-sdk/authentication#token-details).
-
-This is the recommended approach for client-side authentication, for the following reasons:
-
-* An Ably `TokenRequest` can be generated securely by your servers without communicating with Ably.
-* Your secret API key is never shared with Ably or your clients.
-* An Ably `TokenRequest` cannot be tampered with due to being signed, must be used soon after creation, and can only be used once.
-
-The process used by Ably SDKs to authenticate with Ably using a `TokenRequest` is illustrated in the following diagram:
-
-
-
-The following is an example of creating an Ably `TokenRequest`:
-
-
-```javascript
-const ably = new Ably.Rest({ key: '{{API_KEY}}' });
-const tokenRequest = await ably.auth.createTokenRequest({ clientId: 'client@example.com' });
-```
-
-```python
-ably = AblyRest('{{API_KEY}}')
-token = await ably.auth.create_token_request(
-{
- "clientId": "client@example.com",
- "capability": {
- "channel1": ["publish", "subscribe"],
- },
- 'ttl': 3600 * 1000, # ms
-})
-```
-
-```java
-ClientOptions options = new ClientOptions("{{API_KEY}}");
-AblyRest rest = new AblyRest(options);
-
-Auth.TokenParams tokenParams = new Auth.TokenParams();
-tokenParams.clientId = "client@example.com";
-
-Auth.TokenRequest tokenDetails = rest.auth.createTokenRequest(tokenParams, null);
-```
-
-```php
-$rest = new Ably\AblyRest(
- ['key' => '{{API_KEY}}']
-);
-
-$tokenRequest = $rest->auth->createTokenRequest(
- ['clientId' => 'client@example.com']
-);
-```
-
-```go
-rest, err := ably.NewREST(
- ably.WithKey("{{API_KEY}}"))
-if err != nil {
- log.Fatalf("Error creating Ably client: %v", err)
-}
-
-tokenParams := &ably.TokenParams{ClientID: "client@example.com"}
-tokenRequest, _ := rest.Auth.CreateTokenRequest(tokenParams)
-```
-
-```flutter
-final clientOptions = ably.ClientOptions(
- key: '{{API_KEY}}',
-);
-final rest = ably.Rest(options: clientOptions);
-const tokenParams = ably.TokenParams(
- clientId: 'client@example.com'
-);
-final tokenRequest = rest.auth.createTokenRequest(tokenParams: tokenParams);
-```
-
-
-Clients can pass this server-side generated `TokenRequest` to Ably to authenticate with Ably automatically.
-
-### Ably Token
-
-Using an Ably SDK, an Ably Token is [requested by your servers](/docs/api/realtime-sdk/authentication#request-token) from Ably and then passed to the client-side SDK instance. The client-side SDK instance then uses that [Ably Token](/docs/api/realtime-sdk/authentication#tokens) to authenticate with Ably. This is an alternative approach for authentication that enables you to issue"Ably Tokens directly as opposed to providing Ably `TokenRequests` from your servers.
-
-The advantage for clients is that it saves one round trip request as they do not need to request an Ably Token themselves. The disadvantage is that your servers must communicate with Ably each time an Ably Token is required.
-
-The process used by Ably SDKs to authenticate with Ably using an Ably Token is illustrated in the following diagram:
-
-
-
-The following is an example of issuing an Ably Token from a server:
-
-
-```javascript
-const ably = new Ably.Rest({ key: '{{API_KEY}}' });
-const tokenDetails = await ably.auth.requestToken({ clientId: 'client@example.com' });
-```
-
-```python
-rest = AblyRest(key='{{API_KEY}}')
-token_request_params = {
- 'clientId': 'client@example.com',
-}
-
-token_details = await rest.auth.request_token(token_params=token_request_params)
-```
-
-```java
-ClientOptions options = new ClientOptions("{{API_KEY}}");
-AblyRest rest = new AblyRest(options);
-
-Auth.TokenParams tokenParams = new Auth.TokenParams();
-tokenParams.clientId = "client@example.com";
-
-Auth.TokenDetails tokenDetails = rest.auth.requestToken(tokenParams, null);
-```
-
-```php
-$rest = new Ably\AblyRest(
- ['key' => '{{API_KEY}}']
-);
-
-$tokenDetails = $rest->auth->requestToken(
- ['clientId' => 'client@example.com']
-);
-```
-
-```go
-rest, err := ably.NewREST(
- ably.WithKey("API_KEY"))
-if err != nil {
- log.Fatalf("Error creating Ably client: %v", err)
-}
-
-tokenParams := &ably.TokenParams{ClientID: "client@example.com"}
-tokenRequest, _ := rest.Auth.RequestToken(context.Background(), tokenParams)
-```
-
-```flutter
-final clientOptions = ably.ClientOptions(
- key: '{{API_KEY}}',
-);
-final rest = ably.Rest(options: clientOptions);
-
-const tokenParams = ably.TokenParams(
- clientId: 'client@example.com',
-);
-final tokenDetails = await rest.auth.requestToken(
- tokenParams: tokenParams
-);
-```
-
-
## JSON Web Tokens (JWT)
JSON Web Tokens (JWT) can be used to authenticate with Ably in the following ways:
@@ -692,8 +452,6 @@ JSON Web Tokens (JWT) can be used to authenticate with Ably in the following way
* [Ably JWT](#standard) is created by your servers and passed to clients.
* [Ably Token](#embedded) is embedded in a JWT from your server and passed to clients.
-Note that the machine on which you are running your auth server should have an accurate clock, as tokens contain a timestamp. You can use an [NTP daemon](https://en.wikipedia.org/wiki/Ntpd), or if you are not able to control your server's clock, you can wish to use the `queryTime` [auth option](/docs/api/rest-sdk/types#auth-options).
-
### JWT using your API key
It is possible to use a [JWT](https://jwt.io) as a form of token for authentication with Ably, so long as it is structured appropriately, in what will be referred to as an [Ably JWT](/docs/api/realtime-sdk/authentication#ably-jwt). It is possible for an Ably JWT to contain claims indicating its `clientId`, capabilities and expiry - in an analogous way to an [Ably Token](#tokens) - and it is signed with the applicable [Ably API key's secret part](/docs/auth#api-key).
@@ -1082,6 +840,385 @@ final ablyJwt = "$base64Header.$base64Claims.$signature";
```
+## Ably Tokens
+
+Ably Tokens can be used to authenticate with Ably in the following ways:
+
+* Ably [TokenRequest](#token-request) is created by your servers and passed to clients.
+* An [Ably Token](#ably-token) is issued by your servers and passed to clients.
+
+Note that the machine on which you are running your auth server should have an accurate clock, as tokens and `TokenRequest` contain a timestamp. You can use an [NTP daemon](https://en.wikipedia.org/wiki/Ntpd), or if you are not able to control your server's clock, you can wish to use the `queryTime` [auth option](/docs/api/rest-sdk/types#auth-options).
+
+### Ably TokenRequest
+
+Using an Ably SDK, a `TokenRequest` is [generated from your server](/docs/api/realtime-sdk/authentication#create-token-request) and returned to the client-side SDK instance. The client-side SDK instance then uses the [`TokenRequest`](/docs/api/realtime-sdk/types#token-request) to request an [Ably Token](/docs/api/realtime-sdk/authentication#request-token) from Ably, and subsequently authenticates using that [Ably Token](/docs/api/realtime-sdk/authentication#token-details).
+
+TokenRequest is an alternative to JWT when you need to keep capabilities confidential or when your capability list is very large. Key characteristics:
+
+* A `TokenRequest` can be generated securely by your servers without communicating with Ably.
+* Your secret API key is never shared with Ably or your clients.
+* A `TokenRequest` cannot be tampered with due to being signed, must be used soon after creation, and can only be used once.
+
+The process used by Ably SDKs to authenticate with Ably using a `TokenRequest` is illustrated in the following diagram:
+
+
+
+The following is an example of creating an Ably `TokenRequest`:
+
+
+```javascript
+const ably = new Ably.Rest({ key: '{{API_KEY}}' });
+const tokenRequest = await ably.auth.createTokenRequest({ clientId: 'client@example.com' });
+```
+
+```python
+ably = AblyRest('{{API_KEY}}')
+token = await ably.auth.create_token_request(
+{
+ "clientId": "client@example.com",
+ "capability": {
+ "your-channel": ["publish", "subscribe"],
+ },
+ 'ttl': 3600 * 1000, # ms
+})
+```
+
+```java
+ClientOptions options = new ClientOptions("{{API_KEY}}");
+AblyRest rest = new AblyRest(options);
+
+Auth.TokenParams tokenParams = new Auth.TokenParams();
+tokenParams.clientId = "client@example.com";
+
+Auth.TokenRequest tokenDetails = rest.auth.createTokenRequest(tokenParams, null);
+```
+
+```php
+$rest = new Ably\AblyRest(
+ ['key' => '{{API_KEY}}']
+);
+
+$tokenRequest = $rest->auth->createTokenRequest(
+ ['clientId' => 'client@example.com']
+);
+```
+
+```go
+rest, err := ably.NewREST(
+ ably.WithKey("{{API_KEY}}"))
+if err != nil {
+ log.Fatalf("Error creating Ably client: %v", err)
+}
+
+tokenParams := &ably.TokenParams{ClientID: "client@example.com"}
+tokenRequest, _ := rest.Auth.CreateTokenRequest(tokenParams)
+```
+
+```flutter
+final clientOptions = ably.ClientOptions(
+ key: '{{API_KEY}}',
+);
+final rest = ably.Rest(options: clientOptions);
+const tokenParams = ably.TokenParams(
+ clientId: 'client@example.com'
+);
+final tokenRequest = rest.auth.createTokenRequest(tokenParams: tokenParams);
+```
+
+
+Clients can pass this server-side generated `TokenRequest` to Ably to authenticate with Ably automatically.
+
+### Ably Token
+
+Using an Ably SDK, an Ably Token is [requested by your servers](/docs/api/realtime-sdk/authentication#request-token) from Ably and then passed to the client-side SDK instance. The client-side SDK instance then uses that [Ably Token](/docs/api/realtime-sdk/authentication#tokens) to authenticate with Ably. This is an alternative approach for authentication that enables you to issue Ably Tokens directly as opposed to providing Ably `TokenRequests` from your servers.
+
+The advantage for clients is that it saves one round trip request as they do not need to request an Ably Token themselves. The disadvantage is that your servers must communicate with Ably each time an Ably Token is required.
+
+The process used by Ably SDKs to authenticate with Ably using an Ably Token is illustrated in the following diagram:
+
+
+
+The following is an example of issuing an Ably Token from a server:
+
+
+```javascript
+const ably = new Ably.Rest({ key: '{{API_KEY}}' });
+const tokenDetails = await ably.auth.requestToken({ clientId: 'client@example.com' });
+```
+
+```python
+rest = AblyRest(key='{{API_KEY}}')
+token_request_params = {
+ 'clientId': 'client@example.com',
+}
+
+token_details = await rest.auth.request_token(token_params=token_request_params)
+```
+
+```java
+ClientOptions options = new ClientOptions("{{API_KEY}}");
+AblyRest rest = new AblyRest(options);
+
+Auth.TokenParams tokenParams = new Auth.TokenParams();
+tokenParams.clientId = "client@example.com";
+
+Auth.TokenDetails tokenDetails = rest.auth.requestToken(tokenParams, null);
+```
+
+```php
+$rest = new Ably\AblyRest(
+ ['key' => '{{API_KEY}}']
+);
+
+$tokenDetails = $rest->auth->requestToken(
+ ['clientId' => 'client@example.com']
+);
+```
+
+```go
+rest, err := ably.NewREST(
+ ably.WithKey("API_KEY"))
+if err != nil {
+ log.Fatalf("Error creating Ably client: %v", err)
+}
+
+tokenParams := &ably.TokenParams{ClientID: "client@example.com"}
+tokenRequest, _ := rest.Auth.RequestToken(context.Background(), tokenParams)
+```
+
+```flutter
+final clientOptions = ably.ClientOptions(
+ key: '{{API_KEY}}',
+);
+final rest = ably.Rest(options: clientOptions);
+
+const tokenParams = ably.TokenParams(
+ clientId: 'client@example.com',
+);
+final tokenDetails = await rest.auth.requestToken(
+ tokenParams: tokenParams
+);
+```
+
+
+## Common architectural scenarios
+
+### Standard application
+
+For web and mobile applications, create an endpoint that validates users and returns Ably JWTs. This works with any authentication system—sessions, JWT middleware (Auth0, Firebase, Cognito), or custom auth.
+
+**Server (no Ably SDK required):**
+
+
+```rest_nodejs
+import jwt from 'jsonwebtoken';
+
+const [keyName, keySecret] = process.env.ABLY_API_KEY.split(':');
+
+app.get('/api/ably-token', async (req, res) => {
+ // Validate user with your auth system (session, JWT middleware, etc.)
+ const userId = req.user?.id || req.session?.userId;
+ if (!userId) {
+ return res.status(401).json({ error: 'Not authenticated' });
+ }
+
+ const ablyJwt = jwt.sign(
+ {
+ 'x-ably-capability': JSON.stringify({
+ '*': ['publish', 'subscribe', 'presence'],
+ }),
+ 'x-ably-clientId': userId,
+ },
+ keySecret,
+ { algorithm: 'HS256', keyid: keyName, expiresIn: '1h' }
+ );
+
+ res.send(ablyJwt);
+});
+```
+
+```rest_python
+from flask import Flask, request, session
+import jwt
+import os
+import json
+import time
+
+app = Flask(__name__)
+api_key = os.environ['ABLY_API_KEY']
+key_name, key_secret = api_key.split(':')
+
+@app.route('/api/ably-token')
+def get_token():
+ # Validate user with your auth system
+ user_id = getattr(request, 'user', {}).get('id') or session.get('user_id')
+ if not user_id:
+ return {'error': 'Not authenticated'}, 401
+
+ now = int(time.time())
+ ably_jwt = jwt.encode(
+ {
+ 'iat': now,
+ 'exp': now + 3600,
+ 'x-ably-capability': json.dumps({
+ '*': ['publish', 'subscribe', 'presence'],
+ }),
+ 'x-ably-clientId': user_id,
+ },
+ key_secret,
+ algorithm='HS256',
+ headers={'kid': key_name}
+ )
+
+ return ably_jwt
+```
+
+
+**Client:**
+
+
+```realtime_javascript
+const realtime = new Ably.Realtime({
+ authCallback: async (tokenParams, callback) => {
+ try {
+ const response = await fetch('/api/ably-token');
+ const token = await response.text();
+ callback(null, token);
+ } catch (error) {
+ callback(error, null);
+ }
+ },
+});
+```
+
+
+### IoT and serverless
+
+For IoT devices and serverless functions, JWT is ideal because it requires no SDK and is stateless:
+
+**Serverless function (AWS Lambda, Cloud Functions):**
+
+
+```rest_nodejs
+import jwt from 'jsonwebtoken';
+
+const [keyName, keySecret] = process.env.ABLY_API_KEY.split(':');
+
+export const handler = async (event) => {
+ const userId = event.requestContext.authorizer.claims.sub;
+
+ const ablyJwt = jwt.sign(
+ {
+ 'x-ably-capability': JSON.stringify({ '*': ['*'] }),
+ 'x-ably-clientId': userId,
+ },
+ keySecret,
+ { algorithm: 'HS256', keyid: keyName, expiresIn: '1h' }
+ );
+
+ return { statusCode: 200, body: ablyJwt };
+};
+```
+
+
+**IoT device provisioning:**
+
+
+```rest_python
+import jwt
+import time
+import json
+import os
+
+def create_device_jwt(device_id: str) -> str:
+ api_key = os.environ['ABLY_API_KEY']
+ key_name, key_secret = api_key.split(':')
+
+ now = int(time.time())
+ return jwt.encode(
+ {
+ 'iat': now,
+ 'exp': now + 86400, # 24 hours
+ 'x-ably-capability': json.dumps({
+ f'devices:{device_id}': ['publish'],
+ 'control': ['subscribe'],
+ }),
+ 'x-ably-clientId': device_id,
+ },
+ key_secret,
+ algorithm='HS256',
+ headers={'kid': key_name}
+ )
+```
+
+
+Devices can use this JWT with any MQTT client. See [MQTT with JWT authentication](/docs/protocols/mqtt#jwt-auth) for examples.
+
+### Multi-tenant SaaS with user roles
+
+**Recommendation: Ably JWT with channel claims**
+
+When you need to pass trusted user metadata (roles, permissions) to channels:
+
+**Server:**
+
+
+```rest_nodejs
+import jwt from 'jsonwebtoken';
+
+const [keyName, keySecret] = process.env.ABLY_API_KEY.split(':');
+
+function createTenantJwt(userId, tenantId, role) {
+ return jwt.sign(
+ {
+ 'x-ably-capability': JSON.stringify({
+ [`tenant:${tenantId}:*`]: ['publish', 'subscribe', 'presence'],
+ }),
+ 'x-ably-clientId': userId,
+ // Channel-scoped claims - JWT only feature
+ [`ably.channel.tenant:${tenantId}:chat`]: JSON.stringify({
+ role: role,
+ permissions: role === 'admin' ? ['moderate', 'delete'] : ['read'],
+ }),
+ },
+ keySecret,
+ { algorithm: 'HS256', keyid: keyName, expiresIn: '1h' }
+ );
+}
+```
+
+
+Other clients can read these trusted claims from presence or message metadata.
+
+### Rate-limited publishing
+
+**Recommendation: Ably JWT with rate limits**
+
+When you need to restrict how fast specific clients can publish:
+
+**Server:**
+
+
+```rest_nodejs
+import jwt from 'jsonwebtoken';
+
+const [keyName, keySecret] = process.env.ABLY_API_KEY.split(':');
+
+function createRateLimitedJwt(userId) {
+ return jwt.sign(
+ {
+ 'x-ably-capability': JSON.stringify({ '*': ['publish', 'subscribe'] }),
+ 'x-ably-clientId': userId,
+ // Per-connection rate limit - JWT only feature
+ 'ably.limits.publish.perAttachment.maxRate.*': 10, // 10 msgs/sec max on all channels
+ },
+ keySecret,
+ { algorithm: 'HS256', keyid: keyName, expiresIn: '1h' }
+ );
+}
+```
+
+
## Dynamic channel access control
Token authentication allows you to dynamically change a client's channel access permissions without disconnecting. Use the [`authorize()`](/docs/api/realtime-sdk/authentication#authorize) method to re-authenticate with updated [capabilities](/docs/auth/capabilities).
@@ -1124,17 +1261,3 @@ For security purposes, handle non-compliant clients by:
-
-## When to use token auth
-
-Ably recommends that token authentication is used client-side for the following reasons:
-
-* Tokens ensure that an Ably API key isn't exposed in client applications.
-* Tokens are short-lived so there is only a short period of time during which a compromised token can be used.
-* Tokens provide more fine-grained access control, which also limits the area of exposure a compromised token can access.
-
-
diff --git a/src/pages/docs/chat/authentication.mdx b/src/pages/docs/chat/authentication.mdx
new file mode 100644
index 0000000000..8794a31605
--- /dev/null
+++ b/src/pages/docs/chat/authentication.mdx
@@ -0,0 +1,113 @@
+---
+title: Chat authentication
+meta_description: "Configure authentication for Chat applications with the required capabilities."
+---
+
+This page covers Chat-specific capabilities and room scoping. For client setup and server implementation, see [SDK setup](/docs/chat/setup).
+
+## Chat capabilities
+
+Capabilities are permissions that control what operations a client can perform. When you create a token for a Chat user, you specify which capabilities they have. Each Chat feature requires specific capabilities:
+
+| Feature | Required Capabilities |
+|---------|----------------------|
+| Send messages | `publish` |
+| Receive messages | `subscribe` |
+| Update messages | `message-update-any` or `message-update-own` |
+| Delete messages | `message-delete-any` or `message-delete-own` |
+| Message history | `subscribe`, `history` |
+| Message reactions | `annotation-publish`, optionally `annotation-subscribe` |
+| Presence | `subscribe`, `presence` |
+| Typing indicators | `publish`, `subscribe` |
+| Room reactions | `publish`, `subscribe` |
+| Occupancy | `subscribe`, `channel-metadata` |
+| **All Chat features** | `publish`, `subscribe`, `presence`, `history`, `channel-metadata`, `annotation-publish`, `annotation-subscribe`, `message-update-own`, `message-delete-own` |
+
+## Room-scoped capabilities
+
+You can restrict tokens to specific chat rooms. Chat rooms use a channel naming format of `your-room::$chat`:
+
+
+```javascript
+// Server-side JWT with room-scoped capabilities
+import jwt from 'jsonwebtoken';
+
+const [keyName, keySecret] = process.env.ABLY_API_KEY.split(':');
+
+const ablyJwt = jwt.sign(
+ {
+ 'x-ably-capability': JSON.stringify({
+ // Only allow access to a specific room
+ 'your-room::$chat': ['publish', 'subscribe', 'presence', 'history'],
+ }),
+ 'x-ably-clientId': userId,
+ },
+ keySecret,
+ { algorithm: 'HS256', keyid: keyName, expiresIn: '1h' }
+);
+```
+
+```python
+# Server-side JWT with room-scoped capabilities
+import jwt
+import json
+import time
+import os
+
+key_name, key_secret = os.environ['ABLY_API_KEY'].split(':')
+
+now = int(time.time())
+ably_jwt = jwt.encode(
+ {
+ 'iat': now,
+ 'exp': now + 3600,
+ 'x-ably-capability': json.dumps({
+ # Only allow access to a specific room
+ 'your-room::$chat': ['publish', 'subscribe', 'presence', 'history'],
+ }),
+ 'x-ably-clientId': user_id,
+ },
+ key_secret,
+ algorithm='HS256',
+ headers={'kid': key_name}
+)
+```
+
+
+
+
+## Client setup
+
+Use `authCallback` to fetch JWTs from your auth server:
+
+
+```javascript
+import * as Ably from 'ably';
+import { ChatClient } from '@ably/chat';
+
+const realtimeClient = new Ably.Realtime({
+ authCallback: async (tokenParams, callback) => {
+ try {
+ const response = await fetch('/api/ably-token');
+ const jwt = await response.text();
+ callback(null, jwt);
+ } catch (error) {
+ callback(error, null);
+ }
+ },
+});
+
+const chatClient = new ChatClient(realtimeClient);
+```
+
+
+See [token authentication](/docs/auth/token#auth-callback) for complete examples in all languages.
+
+## Further reading
+
+- **Token lifecycle**: Tokens expire and the SDK automatically refreshes them via your `authCallback` - see [Token authentication](/docs/auth/token)
+- **Revoke access**: You can instantly revoke tokens for compromised or banned users - see [Token revocation](/docs/auth/revocation)
+- **Dynamic permissions**: Users can get new capabilities by re-authenticating (e.g., when a moderator grants permissions) - see [Capabilities](/docs/auth/capabilities)
+- **Large capability sets**: If your capability JSON exceeds JWT size limits, use an Ably TokenRequest instead - see [Token authentication](/docs/auth/token#token-request)
diff --git a/src/pages/docs/chat/setup.mdx b/src/pages/docs/chat/setup.mdx
index fdb0aafdaf..c14d4cf459 100644
--- a/src/pages/docs/chat/setup.mdx
+++ b/src/pages/docs/chat/setup.mdx
@@ -11,19 +11,23 @@ If you have any feedback or feature requests, [let us know](https://forms.gle/Sm
## Authentication
-An API key is required to authenticate with Ably. API keys are used either to authenticate directly with Ably using basic authentication, or to generate tokens for untrusted clients using [token authentication](/docs/auth/token).
-
-[Sign up](https://ably.com/sign-up) to Ably to create an API key in the dashboard or use the [Control API](/docs/platform/account/control-api) to create an API key programmatically.
+Chat requires an authenticated client with a `clientId` to identify users. The recommended approach is:
+
+1. **Client-side apps** (browsers, iOS, Android): Use [JWT authentication](/docs/auth/token#choosing-jwt) with `authCallback` to fetch JWTs from your server
+2. **Server-side apps** (Node.js, Python, etc.): Use your API key directly
+
+[Sign up](https://ably.com/sign-up) to Ably to create an API key in the [dashboard](https://ably.com/dashboard) or use the [Control API](/docs/platform/account/control-api) to create an API key programmatically. Your server will use this key to issue tokens to clients.
API keys and tokens have a set of [capabilities](/docs/auth/capabilities) assigned to them that specify which operations, such as subscribe or publish can be performed on which resources. To use the Chat SDK, the API key requires the following capabilities depending on which features are being used:
| Feature | Capabilities |
| ------- | ------------ |
-| Send and receive messages | `publish`, `subscribe` |
+| Send messages | `publish` |
+| Receive messages | `subscribe` |
| Update message | `message-update-any` or `message-update-own` |
| Delete message | `message-delete-any` or `message-delete-own` |
| Message history | `subscribe`, `history` |
@@ -35,11 +39,21 @@ API keys and tokens have a set of [capabilities](/docs/auth/capabilities) assign
When setting the capabilities for Chat, you can apply them to specific chat rooms, a group of chat rooms in a common namespace, or all chat rooms:
-* `my-chat-room` or
-* `dms:*` or
-* `*`
+* `my-chat-room::$chat` - a specific room
+* `dms:*::$chat` - all rooms in the `dms:` namespace
+* `*::$chat` - all chat rooms
+
+The `::$chat` suffix matches the internal channel name the Chat SDK uses. When calling `rooms.get('my-room')`, the SDK automatically uses `my-room::$chat` as the underlying channel.
+
+For more guidance, see the [capabilities documentation](/docs/auth/capabilities) and [Chat authentication](/docs/chat/authentication) for room-scoped examples.
+
+### Client identification
+
+Every Chat client must have a `clientId` - this is a **hard requirement**. The Chat SDK uses the `clientId` to identify who sends messages, who is present in a room, and who is typing.
+
+Your auth server sets the `clientId` when creating tokens. This ensures users can't impersonate each other - the identity is controlled server-side, not by the client.
-For more guidance, see the [capabilities documentation](/docs/auth/capabilities).
+If you try to connect without a `clientId`, the connection will fail.
## Install
@@ -82,7 +96,18 @@ Reference the Pub/Sub SDK and the Chat SDK within your HTML file:
```
@@ -137,24 +162,52 @@ For groovy:
## Instantiate a client
+Authentication is configured on the Ably Pub/Sub client, which the Chat client wraps. The Chat SDK itself doesn't handle authentication directly - it uses the authenticated connection from the underlying Pub/Sub client.
+
Instantiate a realtime client using the Pub/Sub SDK and pass the generated client into the Chat constructor. **It is strongly recommended that you initialise the clients outside of the React component tree** to avoid creating unnecessary additional connections to Ably caused by re-renders.
Pass the `ChatClient` into the [`ChatClientProvider`](https://sdk.ably.com/builds/ably/ably-chat-js/main/typedoc/functions/chat-react.ChatClientProvider.html). The `ChatClient` instance will be available to all child components in your React component tree.
+### Client-side authentication (recommended)
+
+Use token authentication for browsers and mobile apps. Your auth server endpoint validates the user and returns an Ably token with the appropriate `clientId`:
+
```javascript
+// Client-side: Token authentication (recommended for browsers)
import { LogLevel } from '@ably/chat'
-const realtimeClient = new Ably.Realtime({ key: '{{API_KEY}}', clientId: ''});
+const realtimeClient = new Ably.Realtime({
+ authCallback: async (tokenParams, callback) => {
+ try {
+ const response = await fetch('/api/ably-token');
+ const token = await response.text();
+ callback(null, token);
+ } catch (error) {
+ callback(error, null);
+ }
+ },
+});
const chatClient = new ChatClient(realtimeClient, { logLevel: LogLevel.Error });
```
```react
+// Client-side: Token authentication (recommended for React apps)
import { LogLevel } from '@ably/chat'
-const realtimeClient = new Ably.Realtime({ key: '{{API_KEY}}', clientId: ''});
+const realtimeClient = new Ably.Realtime({
+ authCallback: async (tokenParams, callback) => {
+ try {
+ const response = await fetch('/api/ably-token');
+ const token = await response.text();
+ callback(null, token);
+ } catch (error) {
+ callback(error, null);
+ }
+ },
+});
const chatClient = new ChatClient(realtimeClient, { logLevel: LogLevel.Error });
const App = () => {
@@ -167,22 +220,42 @@ const App = () => {
```
```swift
+// Client-side: Token authentication (recommended for iOS apps)
let realtimeOptions = ARTClientOptions()
-realtimeOptions.key = "{{API_KEY}}"
-realtimeOptions.clientId = ""
+realtimeOptions.authCallback = { tokenParams, callback in
+ // Fetch token from your auth server
+ fetchAblyToken { result in
+ switch result {
+ case .success(let tokenRequest):
+ callback(tokenRequest, nil)
+ case .failure(let error):
+ callback(nil, error)
+ }
+ }
+}
let realtime = ARTRealtime(options: realtimeOptions)
let chatClient = ChatClient(realtime: realtime)
```
```kotlin
+// Client-side: Token authentication (recommended for Android apps)
import com.ably.chat.ChatClient
import io.ably.lib.realtime.AblyRealtime
import io.ably.lib.types.ClientOptions
val realtimeClient = AblyRealtime(
ClientOptions().apply {
- key = "{{API_KEY}}"
- clientId = ""
+ authCallback = { tokenParams, callback ->
+ // Fetch token from your auth server
+ fetchAblyToken { result ->
+ result.onSuccess { tokenRequest ->
+ callback.onSuccess(tokenRequest)
+ }
+ result.onFailure { error ->
+ callback.onError(ErrorInfo(error.message, 40000, 401))
+ }
+ }
+ }
},
)
@@ -190,9 +263,118 @@ val chatClient = ChatClient(realtimeClient)
```
-A [`ClientOptions`](/docs/api/realtime-sdk#client-options) object may be passed to the Pub/Sub SDK instance to further customize the connection, however at a minimum you must set an API key and provide a `clientId` to ensure that the client is [identified](/docs/auth/identified-clients).
+Your auth server endpoint (`/api/ably-token`) should authenticate the user and return a token. See the [token authentication](/docs/auth/token) documentation for server implementation examples.
+
+### Server-side authentication
+
+For server-side applications or local development, you can use an API key directly:
+
+
+```javascript
+// Server-side only: API key authentication
+// WARNING: Never use this in client-side code (browsers, mobile apps)
+import { LogLevel } from '@ably/chat'
+
+const realtimeClient = new Ably.Realtime({
+ key: process.env.ABLY_API_KEY,
+ clientId: 'server-process-1',
+});
+const chatClient = new ChatClient(realtimeClient, { logLevel: LogLevel.Error });
+```
+
+
+
+
+A [`ClientOptions`](/docs/api/realtime-sdk#client-options) object may be passed to the Pub/Sub SDK instance to further customize the connection. When using token authentication, the `clientId` is set by your auth server. When using API key authentication server-side, you must provide a `clientId` to ensure that the client is [identified](/docs/auth/identified-clients).
+
+### Using Ably JWT (alternative)
+
+If you have existing JWT-based authentication infrastructure (Auth0, Firebase, Cognito, or custom), you can create Ably JWTs directly without using the Ably SDK on your server:
+
+**Server (no Ably SDK required):**
+
+
+```javascript
+import jwt from 'jsonwebtoken';
+
+const [keyName, keySecret] = process.env.ABLY_API_KEY.split(':');
+
+app.get('/api/ably-jwt', async (req, res) => {
+ // Your existing auth middleware validates the user
+ const userId = req.user.id;
+
+ const ablyJwt = jwt.sign(
+ {
+ 'x-ably-capability': JSON.stringify({
+ '*': ['publish', 'subscribe', 'presence', 'history'],
+ }),
+ 'x-ably-clientId': userId,
+ },
+ keySecret,
+ { algorithm: 'HS256', keyid: keyName, expiresIn: '1h' }
+ );
+
+ res.send(ablyJwt);
+});
+```
+
+
+**Client:**
+
+
+```javascript
+const realtimeClient = new Ably.Realtime({
+ authCallback: async (tokenParams, callback) => {
+ try {
+ const response = await fetch('/api/ably-jwt');
+ const jwt = await response.text();
+ callback(null, jwt);
+ } catch (error) {
+ callback(error, null);
+ }
+ },
+});
+const chatClient = new ChatClient(realtimeClient);
+```
+
+```swift
+let realtimeOptions = ARTClientOptions()
+realtimeOptions.authCallback = { tokenParams, callback in
+ fetchAblyJwt { result in
+ switch result {
+ case .success(let jwt):
+ callback(jwt as ARTTokenDetailsCompatible, nil)
+ case .failure(let error):
+ callback(nil, error)
+ }
+ }
+}
+let realtime = ARTRealtime(options: realtimeOptions)
+let chatClient = ChatClient(realtime: realtime)
+```
+
+```kotlin
+val realtimeClient = AblyRealtime(
+ ClientOptions().apply {
+ authCallback = Auth.TokenCallback { _ ->
+ // Fetch JWT from your server
+ fetchAblyJwt()
+ }
+ }
+)
+val chatClient = ChatClient(realtimeClient)
+```
+
+
+**Why choose JWT for Chat?**
+- No Ably SDK required on your server
+- Integrates with existing Auth0/Firebase/Cognito flows
+- Supports [channel-scoped claims](/docs/auth/capabilities#custom-restrictions) for user roles in chat rooms
+- Eliminates client round-trip to Ably
-In many cases, a users unique application-specific identifier may be used as the `clientId` to provide consistent identification for clients across your application.
+See [Choosing a token mechanism](/docs/auth/token#choosing) for detailed guidance on when to use JWT vs TokenRequest.
Additional options can also be passed to the Chat client to customize the following properties:
@@ -229,7 +411,13 @@ Set the `logHandler` and `logLevel` properties when [instantiating a client](#in
```javascript
-const ably = new Ably.Realtime({ key: '{{API_KEY}}', clientId: ''});
+// Using token authentication (recommended for client-side)
+const ably = new Ably.Realtime({
+ authCallback: async (tokenParams, callback) => {
+ const response = await fetch('/api/ably-token');
+ callback(null, await response.text());
+ },
+});
const chatClient = new ChatClient(ably, {logHandler: logWriteFunc, logLevel: 'debug' });
```
@@ -238,10 +426,16 @@ import * as Ably from 'ably';
import { LogLevel } from '@ably/chat';
import { ChatClientProvider } from '@ably/chat/react';
-const ably = new Ably.Realtime({ key: '{{API_KEY}}', clientId: ''});
+// Using token authentication (recommended for React apps)
+const ably = new Ably.Realtime({
+ authCallback: async (tokenParams, callback) => {
+ const response = await fetch('/api/ably-token');
+ callback(null, await response.text());
+ },
+});
const chatClient = new ChatClient(ably, {logHandler: logWriteFunc, logLevel: 'debug' });
-const App = => {
+const App = () => {
return (
@@ -252,18 +446,30 @@ const App = => {
```swift
let realtimeOptions = ARTClientOptions()
-realtimeOptions.key = "{{API_KEY}}"
-realtimeOptions.clientId = ""
+// Using token authentication (recommended for iOS apps)
+realtimeOptions.authCallback = { tokenParams, callback in
+ fetchAblyToken { result in
+ switch result {
+ case .success(let tokenRequest): callback(tokenRequest, nil)
+ case .failure(let error): callback(nil, error)
+ }
+ }
+}
let realtime = ARTRealtime(options: realtimeOptions)
let clientOptions = ChatClientOptions(logHandler: SomeLogHandler(), logLevel: .debug)
return ChatClient(realtime: realtime, clientOptions: clientOptions)
```
```kotlin
+// Using token authentication (recommended for Android apps)
val realtimeClient = AblyRealtime(
ClientOptions().apply {
- key = "{{API_KEY}}"
- clientId = ""
+ authCallback = { tokenParams, callback ->
+ fetchAblyToken { result ->
+ result.onSuccess { callback.onSuccess(it) }
+ result.onFailure { callback.onError(ErrorInfo(it.message, 40000, 401)) }
+ }
+ }
},
)
val chatClient = ChatClient(realtimeClient) {
diff --git a/src/pages/docs/spaces/authentication.mdx b/src/pages/docs/spaces/authentication.mdx
new file mode 100644
index 0000000000..76c7c899ab
--- /dev/null
+++ b/src/pages/docs/spaces/authentication.mdx
@@ -0,0 +1,115 @@
+---
+title: Spaces authentication
+meta_description: "Configure authentication for Spaces applications with the required capabilities."
+---
+
+This page covers Spaces-specific capabilities and space scoping. For client setup and server implementation, see [SDK setup](/docs/spaces/setup).
+
+## Spaces capabilities
+
+Capabilities are permissions that control what operations a client can perform. When you create a token for a Spaces user, you specify which capabilities they have. Each Spaces feature requires specific capabilities:
+
+| Feature | Required Capabilities |
+|---------|----------------------|
+| Avatar stack (member events) | `subscribe`, `presence` |
+| Member locations | `subscribe`, `presence` |
+| Live cursors | `publish`, `subscribe` |
+| Component locking | `subscribe`, `presence` |
+| **All Spaces features** | `publish`, `subscribe`, `presence`, `history` |
+
+## Space-scoped capabilities
+
+You can restrict tokens to specific spaces. Spaces uses multiple channels per space:
+- The main space channel (`your-space`) handles presence and member locations
+- A dedicated cursors channel (`your-space::$cursors`) handles live cursor positions
+
+Cursors have their own channel because cursor updates are very frequent (potentially 60+ times per second per user), while presence and location updates are less frequent. This separation improves performance.
+
+
+```javascript
+// Server-side JWT with space-scoped capabilities
+import jwt from 'jsonwebtoken';
+
+const [keyName, keySecret] = process.env.ABLY_API_KEY.split(':');
+
+const ablyJwt = jwt.sign(
+ {
+ 'x-ably-capability': JSON.stringify({
+ // Main space channel for presence and locations
+ 'your-space': ['publish', 'subscribe', 'presence', 'history'],
+ // Cursors channel
+ 'your-space::$cursors': ['publish', 'subscribe'],
+ }),
+ 'x-ably-clientId': userId,
+ },
+ keySecret,
+ { algorithm: 'HS256', keyid: keyName, expiresIn: '1h' }
+);
+```
+
+```python
+# Server-side JWT with space-scoped capabilities
+import jwt
+import json
+import time
+import os
+
+key_name, key_secret = os.environ['ABLY_API_KEY'].split(':')
+
+now = int(time.time())
+ably_jwt = jwt.encode(
+ {
+ 'iat': now,
+ 'exp': now + 3600,
+ 'x-ably-capability': json.dumps({
+ # Main space channel for presence and locations
+ 'your-space': ['publish', 'subscribe', 'presence', 'history'],
+ # Cursors channel
+ 'your-space::$cursors': ['publish', 'subscribe'],
+ }),
+ 'x-ably-clientId': user_id,
+ },
+ key_secret,
+ algorithm='HS256',
+ headers={'kid': key_name}
+)
+```
+
+
+
+
+## Client setup
+
+Use `authCallback` to fetch JWTs from your auth server:
+
+
+```javascript
+import Spaces from '@ably/spaces';
+import { Realtime } from 'ably';
+
+const client = new Realtime({
+ authCallback: async (tokenParams, callback) => {
+ try {
+ const response = await fetch('/api/ably-token');
+ const jwt = await response.text();
+ callback(null, jwt);
+ } catch (error) {
+ callback(error, null);
+ }
+ },
+});
+
+const spaces = new Spaces(client);
+```
+
+
+See [token authentication](/docs/auth/token#auth-callback) for complete examples in all languages.
+
+## Further reading
+
+- **Token lifecycle**: Tokens expire and the SDK automatically refreshes them via your `authCallback` - see [Token authentication](/docs/auth/token)
+- **Revoke access**: You can instantly revoke tokens for users who leave a collaboration session - see [Token revocation](/docs/auth/revocation)
+- **Dynamic permissions**: Users can get new capabilities by re-authenticating (e.g., when granted edit access) - see [Capabilities](/docs/auth/capabilities)
+- **Large capability sets**: If your capability JSON exceeds JWT size limits, use an Ably TokenRequest instead - see [Token authentication](/docs/auth/token#token-request)
diff --git a/src/pages/docs/spaces/setup.mdx b/src/pages/docs/spaces/setup.mdx
index 5fc928839d..7418307221 100644
--- a/src/pages/docs/spaces/setup.mdx
+++ b/src/pages/docs/spaces/setup.mdx
@@ -7,13 +7,16 @@ Use these instructions to install, authenticate and instantiate the Spaces SDK.
## Authenticate
-An [API key](/docs/auth#api-keys) is required to authenticate with Ably. API keys are used either to authenticate directly with Ably using [basic authentication](/docs/auth/basic), or to generate tokens for untrusted clients using [token authentication](/docs/auth/token).
-
-[Sign up](https://ably.com/sign-up) to Ably to create an API key in the [dashboard](https://ably.com/dashboard) or use the [Control API](/docs/platform/account/control-api) to create an API programmatically.
+Spaces requires an authenticated client with a `clientId` to identify users. The recommended approach is:
+
+1. **Client-side apps** (browsers, mobile apps): Use [JWT authentication](/docs/auth/token#choosing-jwt) with `authCallback` to fetch JWTs from your server
+2. **Server-side apps** (Node.js, Python, etc.): Use your API key directly
+
+[Sign up](https://ably.com/sign-up) to Ably to create an API key in the [dashboard](https://ably.com/dashboard) or use the [Control API](/docs/platform/account/control-api) to create an API programmatically. Your server will use this key to issue tokens to clients.
API keys and tokens have a set of capabilities assigned to them that specify which operations, such as `subscribe` or `publish` can be performed on which resources. To use the Spaces SDK, the API key requires the following [capabilities](/docs/auth/capabilities):
@@ -22,6 +25,16 @@ API keys and tokens have a set of capabilities assigned to them that specify whi
* Presence
* History
+For space-scoped capabilities and channel architecture details, see [Spaces authentication](/docs/spaces/authentication).
+
+### Client identification
+
+Every Spaces client must have a `clientId` - this is a **hard requirement**. The Spaces SDK uses the `clientId` to identify users in the avatar stack, track their locations, display their cursors, and manage component locking.
+
+Your auth server sets the `clientId` when creating tokens. This ensures users can't impersonate each other - the identity is controlled server-side, not by the client.
+
+If you try to connect without a `clientId`, the connection will fail.
+
## Install