Page updated Apr 19, 2024

Add custom real-time subscriptions

Create a custom real-time subscription for any mutation to enable PubSub use cases.

Define a custom subscription

For every custom subscription, you need to set:

  1. the mutation(s) that should trigger a subscription event,
  2. a return type that matches the subscribed mutations' return type,
  3. authorization rules.

Optionally, you can set filter arguments to customize the server-side subscription filter rules.

Use a.subscription() to define your custom subscription in your amplify/data/resource.ts file:

amplify/data/resource.ts
1import { type ClientSchema, a, defineData } from '@aws-amplify/backend';
2
3const schema = a.schema({
4 // Message type that's used for this PubSub sample
5 Message: a.customType({
6 content: a.string().required(),
7 channelName: a.string().required()
8 }),
9
10 // Message publish mutation
11 publish: a.mutation()
12 .arguments({
13 channelName: a.string().required(),
14 content: a.string().required()
15 })
16 .returns(a.ref('Message'))
17 .handler(a.handler.custom({ entry: './publish.js' }))
18 .authorization(allow => [allow.publicApiKey()]),
19
20 // Subscribe to incoming messages
21 receive: a.subscription()
22 // subscribes to the 'publish' mutation
23 .for(a.ref('publish'))
24 // return value matches the `publish` mutation's return value
25 .returns(a.ref('Message'))
26 // subscription handler to set custom filters
27 .handler(a.handler.custom({entry: './receive.js'}))
28 // authorization rules as to who can subscribe to the data
29 .authorization(allow => [allow.publicApiKey()]),
30
31 // A data model to manage channels
32 Channel: a.model({
33 name: a.string(),
34 }).authorization(allow => [allow.publicApiKey()]),
35});
36
37export type Schema = ClientSchema<typeof schema>;
38
39export const data = defineData({
40 schema
41});

For this example, we're building a generic PubSub capability. This requires us to convert the arguments for publish into the Channel's format. Create a new publish.js file in your amplify/data/ folder with the following contents:

amplify/data/publish.js
1// This handler simply passes through the arguments of the mutation through as the result
2export function request() {
3 return {}
4}
5
6/**
7 * @param {import('@aws-appsync/utils').Context} ctx
8 */
9export function response(ctx) {
10 return ctx.args
11}

Next, create a new receive.js file in your amplify/data/ folder to define handlers for your subscription. In this case, it'll just be a simple passthrough. In the next section, we'll explore how to use this handler to construct more advanced subscription filters.

Note: We're planning a developer experience enhancement in the near future that'll create this passthrough under the hood.

amplify/data/receive.js
1export function request() {
2 return {};
3}
4
5export const response = (ctx) => {
6 return ctx.result;
7};

Subscribe to custom subscriptions client-side

From your generated Data client, you can find all your custom subscriptions under client.subscriptions. Subscribe using the .subscribe() function and then use the next function to handle incoming events.

1import { generateClient } from 'aws-amplify/data'
2import type { Schema } from '../amplify/data/resource'
3
4const client = generateClient<Schema>()
5
6const sub = client.subscriptions.receive()
7 .subscribe({
8 next: event => {
9 console.log(event)
10 }
11 }
12)

You can try publishing an event using the custom mutation to test the real-time subscription.

1client.mutations.publish({
2 channelName: "world",
3 content: "My first message!"
4})

Your subscription event should be received and logs the payload into your app's developer console. Unsubscribe your subscription to disconnect using the unsubscribe() function.

1sub.unsubscribe()

(Optionally) Add server-side subscription filters

You can add subscription filters by adding arguments to the custom subscriptions.

If you want to customize the filters, modify the subscription handler. For this example, we'll allow a customer to pass in a namePrefix parameter that allows the end users to only receive channel events in channels that start with the namePrefix.

amplify/data/resource.ts
1import { type ClientSchema, a, defineData } from '@aws-amplify/backend';
2
3const schema = a.schema({
4 Channel: a.model({
5 name: a.string(),
6 }).authorization(allow => [allow.publicApiKey()]),
7
8 Message: a.customType({
9 content: a.string().required(),
10 channelName: a.string().required()
11 }),
12
13 publish: a.mutation()
14 .arguments({
15 channelName: a.string().required(),
16 content: a.string().required()
17 })
18 .returns(a.ref('Message'))
19 .handler(a.handler.custom({ entry: './publish.js' }))
20 .authorization(allow => [allow.publicApiKey()]),
21
22 receive: a.subscription()
23 .for(a.ref('publish'))
24 .arguments({ namePrefix: a.string() })
25 .returns(a.ref('Message'))
26 .handler(a.handler.custom({entry: './receive.js'}))
27 .authorization(allow => [allow.publicApiKey()])
28});
29
30export type Schema = ClientSchema<typeof schema>;
31
32export const data = defineData({
33 schema
34});

In your handler, you can set custom subscription filters based on arguments passed into the custom subscription. For this example, create a new receive.js file alongside the amplify/data/resource.ts file:

1import { util, extensions } from "@aws-appsync/utils"
2
3// Subscription handlers must return a `null` payload on the request
4export function request() { return { payload: null } }
5
6/**
7 * @param {import('@aws-appsync/utils').Context} ctx
8 */
9export function response(ctx) {
10 const filter = {
11 channelName: {
12 beginsWith: ctx.args.namePrefix
13 }
14 }
15
16 extensions.setSubscriptionFilter(util.transform.toSubscriptionFilter(filter))
17
18 return null
19}