# Capabilities
API keys and Ably-compatible tokens, have a set of capabilities assigned to them that specify which operations (such as subscribe or publish) can be performed on which channels.
API keys are long-lived, secret and typically not shared with clients. API key capabilities are configured using the [dashboard](https://ably.com/dashboard), or using the [Control API](https://ably.com/docs/platform/account/control-api).
Ably-compatible tokens are designed to be shared with untrusted clients, are short-lived, and can be configured and issued programmatically. For restricting client access to channels, tokens provide far more flexibility and security than API key capabilities. See [selecting an authentication mechanism](https://ably.com/docs/auth#selecting-auth) to understand why token authentication is the preferred option in most scenarios.
### Capability changes and existing connections
Once created, it's best to think of keys and tokens as being immutable. To modify the permissions granted to existing connections, the recommended approach is to use [token authentication](https://ably.com/docs/auth/token) and issue the client a new token with the updated permissions you want them to have.
The one exception is if a key is revoked entirely, in which case all connections using that key (or a token derived from that key) will be forcibly terminated. See [token revocation](https://ably.com/docs/auth/revocation) for more information.
## Resource names and wildcards
Capabilities are a map from resources to a list of [operations](#capability-operations). Each resource can match a single channel, for example, `channel`, or multiple channels using wildcards (`*`).
Wildcards can only replace whole segments (segments are delimited by `:`) of the resource name. A wildcard at the end of the name can arbitrarily replace many segments. For example:
* A resource of `*` will match any channel, but not queues and metachannels.
* A resource of `namespace:*` will match any channel in the `namespace` namespace, including `namespace:channel`, and `namespace:channel:other`.
* A resource of `foo:*:baz` will match `foo:bar:baz`, but not `foo:bar:bam:baz`.
* A resource of `foo:*` will match expressions such as `foo:bar`, `foo:bar:bam`, `foo:bar:bam:baz`, as the wildcard is at the end.
* A resource of `foo*` (without a colon) will only match the single channel literally called `foo*`.
A resource can also be a queue, in which case it will start with `[queue]`, for example `[queue]appid-queuename`. This is unambiguous as channel names may not begin with a `[`. Similar wildcard rules apply, for example `[queue]*` will match all queues.
A resource can also be a metachannel, in which case it will start with `[meta]`, for example `[meta]metaname`. This is unambiguous as channel names may not begin with a `[`. `[meta]*` will match all metachannels. Just `*` on its own will not: it will match all possible normal channels, but no metachannels.
You can also have a resource name of `[*]*`, which will match all queues, all metachannels, and all channels.
Wildcards are also supported for [operations](#capability-operations), by requesting an operations list of `['*']`.
## Capability operations
The following capability operations are available for API keys and issued tokens:
| Operation | Description |
|-----------|-------------|
| **subscribe** | Can subscribe to messages, objects, and presence state change messages on channels, and get the presence set of a channel |
| **publish** | Can publish messages to channels |
| **presence** | Can register presence on a channel (enter, update and leave) |
| **object-subscribe** | Can subscribe to updates to objects on a channel |
| **object-publish** | Can update objects on a channel |
| **annotation-subscribe** | Can subscribe to individual annotations on a channel |
| **annotation-publish** | Can publish annotations to messages on a channel |
| **message-update-own** | Can [update messages](https://ably.com/docs/messages/updates-deletes) where the original publisher's `clientId` matches the updater's `clientId` |
| **message-update-any** | Can [update any message](https://ably.com/docs/messages/updates-deletes) on the channel |
| **message-delete-own** | Can [delete messages](https://ably.com/docs/messages/updates-deletes) where the original publisher's `clientId` matches the deleter's `clientId` |
| **message-delete-any** | Can [delete any message](https://ably.com/docs/messages/updates-deletes) on the channel |
| **history** | Can retrieve message and presence state history on channels |
| **stats** | Can retrieve current and historical usage statistics for an app |
| **push-subscribe** | Can subscribe devices for push notifications |
| **push-admin** | Can manage device registrations and push subscriptions for all devices in an app |
| **channel-metadata** | Can get metadata for a channel, and enumerate channels |
| **privileged-headers** | Can set data in the privileged section of the [message extras](https://ably.com/docs/api/realtime-sdk/messages#extras) |
Although most capabilities need to be enabled for the resource you're using them with, there are exceptions. The `stats` permission only does something when attached to the wildcard resource `'*'`, or a resource that contains that as a subset, such as `'[*]*'`, since stats are app-wide.
## Channel access control
Ably does not provide numeric limits on channel access. Control channel access using token authentication and capabilities.
Channel access is controlled through:
* [Token authentication](https://ably.com/docs/auth/token) to restrict access by issuing tokens with specific capabilities to authorized users.
* Specific `clientId` values in tokens to ensure only certain users can access particular channels.
* Granting or restricting specific operations (`subscribe`, `publish`, `presence`) on channels through capability configurations.
For private messaging or group chats, design channel naming strategies and use token authentication to ensure users receive tokens with access only to relevant channels.
The `channel-metadata` permission works both ways. When associated with a specific channel or set of channels it allows you to [query the metadata of a channel](https://ably.com/docs/metadata-stats/metadata/rest) to request its status. When associated with the wildcard resource `'*'` it takes on an additional meaning: as well as allowing channel status requests for all channels, it also allows you to [enumerate all active channels](https://ably.com/docs/metadata-stats/metadata/rest#enumerate).
## API key capabilities
An [Ably API key](https://ably.com/docs/auth#api-key) can have a single set of permissions, applied to any number of [channels](https://ably.com/docs/channels) or [queues](https://ably.com/docs/platform/integrations/queues).
You can also choose whether to restrict the API key to only channels, only [queues](https://ably.com/docs/platform/integrations/queues), or to match a set of channel or queue names. If you've chosen to restrict the API key to *selected channels and queues*, you can use a comma separated list of resources the API key can access, making use of wildcards to provide access to areas of your app. It is worth noting an API key will provide the same permissions to all resources it has access to.
To view the capabilities for an existing API key:
1. Sign into your [Ably dashboard](https://ably.com/dashboard).
2. Select the **API Keys** tab.
3. Click the **Settings** button for the key you want to check the capabilities for.
## Token capabilities
Ably Tokens and JWTs are issued from an existing API key and their capabilities can, at most, match the capabilities of the issuing API key.
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:
```javascript
var tokenParams = { clientId: 'foo', capability: JSON.stringify(capability) };
const tokenRequest = await ably.auth.createTokenRequest(tokenParams);
```
```python
token_params = {
'client_id': 'foo',
'capability': json.dumps(capability)
}
token_request = await ably_rest.auth.create_token_request(token_params)
```
```php
$tokenParams = [
'clientId' => 'client@example.com',
'capability' => json_encode($capability)
];
$tokenRequest = $rest->auth->createTokenRequest($tokenParams);
```
```go
rest, _ := ably.NewREST(
ably.WithKey("xVLyHw.I2jW-g:dOTlhEt-nIubVAPMAUJnGv-_F8BZ7xNYnXdajpGaISg"))
// Define the capability
capability := map[string][]string{
"*": {"*"},
}
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)
}
```
```flutter
final tokenParams = ably.TokenParams(
clientId: 'foo',
capability: jsonEncode(capability),
);
final tokenRequest = await rest.auth.createTokenRequest(tokenParams: tokenParams);
```
### Token capability determination
The capabilities for tokens are determined based on those of the issuing API key and those requested by the token.
#### Ably Token without capabilities
If no capability is specified in an Ably `TokenRequest`, then the [Ably Token](https://ably.com/docs/auth/token#tokens) will be given the full set of capabilities assigned to the issuing key.
Using the following example, an API key exists with the listed capabilities. If an Ably Token is requested without specifying any capabilities then the `TokenRequest` is treated as requesting all capabilities, i.e. `{"[*]*":["*"]}`. This will result in the Ably Token receiving all the capabilities of the API key.
```javascript
// API key capabilities:
{
'chat': ['publish', 'subscribe', 'presence'],
'status': ['subscribe']
}
// Token request that doesn't specify any capabilities:
await auth.requestToken(tokenCallback)
// Resulting token capabilities:
{
'chat': ['publish', 'subscribe', 'presence'],
'status': ['subscribe']
}
```
```python
# API key capabilities:
# {
# "chat": ["publish", "subscribe", "presence"],
# "status": ["subscribe"]
# }
// Token request that doesn't specify any capabilities:
token = await ably.auth.create_token_request(
{
"clientId": "client@example.com",
'ttl': 3600 * 1000, # ms
})
# Resulting token capabilities:
# {
# "chat": ["publish", "subscribe", "presence"],
# "status": ["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"]
// }
rest, _ := ably.NewREST(
ably.WithKey("your-api-key"))
// Define the token parameters
tokenParams := &ably.TokenParams{
ClientID: "client@example.com",
TTL: 3600 * 1000, /* time of expiration in ms (an hour) */
}
// Create a token request
tokenRequest, err := rest.Auth.CreateTokenRequest(tokenParams)
if err != nil {
log.Fatalf("Failed to create token request: %v", err)
}
// 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']
// }
```
#### Ably Token with intersection of capabilities
If a set of capabilities are requested, then the Ably Token will be assigned the intersection of the requested capability and the capability of the issuing key.
Using the following example, an API key exists with the listed capabilities. If an [Ably Token](https://ably.com/docs/auth/token#tokens) is requested and specifies a set of capabilities, then the resulting token will only receive those capabilities that intersect. The capabilities of a token cannot exceed those of the issuing API key.
```javascript
// API key capabilities:
{
'chat:*': ['publish', 'subscribe', 'presence'],
'status': ['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
}});
// Resulting token capabilities:
{
'chat:bob': ['subscribe'],
'status': ['subscribe', 'history']
}
```
```python
# 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
}
token_details = await ably_rest.auth.request_token({
'capability': json.dumps(capabilities)
})
# Resulting token capabilities:
# {
# "chat:bob": ["subscribe"],
# "status": ["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"],
// "alerts": ["subscribe"]
// }
// Token request that specifies capabilities:
rest, _ := ably.NewREST(
ably.WithKey("your-api-key"))
// Define the capabilities
capabilities := map[string][]string{
"chat:bob": {"subscribe"},
"status": {"*"},
"secret": {"publish", "subscribe"},
}
capabilitiesJSON, err := json.Marshal(capabilities)
if err != nil {
log.Fatalf("Failed to marshal capabilities: %v", err)
}
// Define the token parameters
tokenParams := &ably.TokenParams{
Capability: string(capabilitiesJSON),
}
// Request a token
tokenDetails, err := rest.Auth.RequestToken(context.Background(), tokenParams)
if err != nil {
log.Fatalf("Failed to request token: %v", err)
}
// 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']
// }
```
#### Ably Token with incompatible capabilities
If a set of capabilities are requested, and the intersection between those and the API key's capabilities is empty, then the `TokenRequest` will result in an error.
Using the following example, an API key exists with the listed capabilities. If an [Ably Token](https://ably.com/docs/auth/token#tokens) is requested that specifies a set of capabilities, and there is no intersection between the capabilities of the issuing API key and requested token, then the token request will be rejected. In the following example, the callback will be returned with an error.
```javascript
// API key capabilities:
{
'chat': ['*']
}
// Token request that specifies capabilities:
const tokenDetails = await auth.requestToken({ capability: {
'status': ['*']
}});
```
```python
# API key capabilities:
# {
# "chat": ["*"]
# }
token_details = await ably_rest.auth.request_token({
'capability': json.dumps({
{
"status": ["*"]
}
})
})
```
```php
/**
* API key capabilities:
* {
* 'chat': ['*']
* }
*/
// Token request that specifies capabilities:
$tokenDetails = $rest
->auth
->requestToken(
['capability' => json_encode(['status' => ['*']])]
);
```
```go
// API key capabilities:
// {
// "chat": ["*"]
// }
rest, _ := ably.NewREST(
ably.WithKey("your-api-key"))
// Define the capabilities
capabilities := map[string][]string{
"status": {"*"},
}
capabilitiesJSON, err := json.Marshal(capabilities)
if err != nil {
log.Fatalf("Failed to marshal capabilities: %v", err)
}
// Define the token parameters
tokenParams := &ably.TokenParams{
Capability: string(capabilitiesJSON),
}
// Request a token
tokenDetails, err := rest.Auth.RequestToken(context.Background(), tokenParams)
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
Capabilities are determined for [Ably JWTs](https://ably.com/docs/auth/token#jwt) in the following way:
* The capabilities granted to an Ably JWT will be the intersection of the capabilities within the Ably JWT and the capabilities of the associated API key.
* If the set of capabilities within the Ably JWT have no intersection with the capabilities of the API key, then an error will instead be returned.
## 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](https://ably.com/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.
To set the trusted fields you need to include `ably.channel.*` in your JWT authentication payload, for example:
```javascript
const 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
}
```
```python
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
}
```
```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",
}
```
```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:
```javascript
const fromModerator = (message) => {
const userClaim = message.extras && message.extras.userClaim;
return (userClaim && userClaim == 'moderator');
}
```
```python
def from_moderator(message):
user_claim = message.get('extras', {}).get('userClaim')
return user_claim == 'moderator' if user_claim else False
```
```go
func fromModerator(message map[string]interface{}) bool {
// Check if 'extras' exists and is a map
if extras, ok := message["extras"].(map[string]interface{}); ok {
// Check if 'userClaim' exists and is a string
if userClaim, ok := extras["userClaim"].(string); ok {
return userClaim == "moderator"
}
}
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.
Rate limits can be scoped to individual channels or to [channel namespaces](https://ably.com/docs/channels#namespaces). Note that the rate limit with the most specific scope will be applied to the user.
To set rate limits for individual connections, include `ably.limits.publish.perAttachment.maxRate.` in your JWT authentication payload. The value of this property sets how many messages can be published per second to a channel, or namespace. For example, a value of `5` restricts the rate to 5 messages per second. A value of `0.1` restricts the rate to 1 message every 10 seconds.
The following is an example of setting different rate limits for different channels:
```javascript
const 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
}
```
```python
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
}
```
```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
}
```
```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
};
```